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,43 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { kindOf } from '../../src/film-doc.mjs';
|
|
3
|
+
|
|
4
|
+
/** Studio's row kind, including audio-only uses of video files.
|
|
5
|
+
* @param {{ clips: readonly { src: string, sound?: boolean }[] }} track
|
|
6
|
+
* @returns {'mg' | 'video' | 'audio'}
|
|
7
|
+
*/
|
|
8
|
+
export function trackKind(track) {
|
|
9
|
+
const kinds = new Set(track.clips.map((clip) => {
|
|
10
|
+
const kind = kindOf(clip);
|
|
11
|
+
return kind === 'page' ? 'mg' : kind === 'sound' ? 'audio' : 'video';
|
|
12
|
+
}));
|
|
13
|
+
return kinds.size === 1 ? /** @type {'mg' | 'video' | 'audio'} */ ([...kinds][0]) : 'video';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Picture rows count bottom up, sound rows top down; each prefix counts separately.
|
|
17
|
+
* @param {readonly string[]} kinds
|
|
18
|
+
*/
|
|
19
|
+
export function rowBadges(kinds) {
|
|
20
|
+
const prefixes = kinds.map((kind) => kind === 'mg' ? 'P' : kind === 'video' ? 'V' : 'A');
|
|
21
|
+
const total = new Map();
|
|
22
|
+
for (const prefix of prefixes) total.set(prefix, (total.get(prefix) ?? 0) + 1);
|
|
23
|
+
const seen = new Map();
|
|
24
|
+
return prefixes.map((prefix) => {
|
|
25
|
+
const nth = (seen.get(prefix) ?? 0) + 1;
|
|
26
|
+
seen.set(prefix, nth);
|
|
27
|
+
return `${prefix}${prefix === 'A' ? nth : total.get(prefix) - nth + 1}`;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Greedy rows in milliseconds; one millisecond of overlap is rounding at a shared edge.
|
|
32
|
+
* @template {{ startMs: number, endMs: number }} T
|
|
33
|
+
* @param {readonly T[]} blocks @returns {T[][]}
|
|
34
|
+
*/
|
|
35
|
+
export function layerBlocks(blocks) {
|
|
36
|
+
/** @type {T[][]} */
|
|
37
|
+
const rows = [];
|
|
38
|
+
for (const block of [...blocks].sort((a, b) => a.startMs - b.startMs || a.endMs - b.endMs)) {
|
|
39
|
+
const row = rows.find((r) => (r.at(-1)?.endMs ?? 0) <= block.startMs + 1);
|
|
40
|
+
if (row) row.push(block); else rows.push([block]);
|
|
41
|
+
}
|
|
42
|
+
return rows;
|
|
43
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio is one place: a project (/projects/<id>). The projects are a popover inside it; `/` goes to the project
|
|
3
|
+
* opened last, or (none left, or only ones whose folder is gone) shows the Projects window on its own, with "New
|
|
4
|
+
* project" in it: no folder is made that the person did not ask for. The address is the state, so the agent's
|
|
5
|
+
* `openfilm open` (which sends the page to a project) and the browser's back button both work.
|
|
6
|
+
*
|
|
7
|
+
* In an app with tabs (lib/host.ts `tabs`) this page is one project's tab: a project picked opens in its own tab (the
|
|
8
|
+
* app's to open), `/` is the projects (the window with no tab open), and `openfilm open` is the app's to settle (every
|
|
9
|
+
* tab hears it).
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useState, useSyncExternalStore } from 'react';
|
|
12
|
+
import { api, listen, studioConnection } from './api';
|
|
13
|
+
import { BrandLoadingScreen } from './components/BrandLoadingScreen';
|
|
14
|
+
import { Launcher } from './components/Launcher';
|
|
15
|
+
import { ProjectsPopover } from './components/ProjectsLibrary';
|
|
16
|
+
import { ProjectView } from './components/ProjectView';
|
|
17
|
+
import { WhatsNew } from './components/WhatsNew';
|
|
18
|
+
import { useT } from './i18n';
|
|
19
|
+
import { studioHost } from './lib/host';
|
|
20
|
+
import { sourcesApi } from './lib/project-sources';
|
|
21
|
+
import { LIST_WAIT_MS, within } from './lib/projects-page';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `to` with this page's key in its address (the server puts it there): the address bar is what a person copies, and
|
|
25
|
+
* with the key a copied address opens Studio in any browser on this machine, or in an agent's preview.
|
|
26
|
+
*/
|
|
27
|
+
const keepKey = (to: string) => {
|
|
28
|
+
const key = new URLSearchParams(location.search).get('key');
|
|
29
|
+
if (!key) return to;
|
|
30
|
+
const at = new URL(to, location.origin);
|
|
31
|
+
at.searchParams.set('key', key);
|
|
32
|
+
return at.pathname + at.search;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const projectIn = (path: string) => /^\/projects\/([\w-]+)/.exec(path)?.[1] ?? null;
|
|
36
|
+
/** the app's tabs, when Studio is a tab of an app's window */
|
|
37
|
+
const tabs = studioHost?.tabs ?? null;
|
|
38
|
+
|
|
39
|
+
export function App() {
|
|
40
|
+
const [path, setPath] = useState(location.pathname);
|
|
41
|
+
const go = (to: string, replace = false) => {
|
|
42
|
+
if (to !== location.pathname) (replace ? history.replaceState : history.pushState).call(history, null, '', keepKey(to));
|
|
43
|
+
setPath(to);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/* the examples and skills, read as Studio opens: a gallery opened later shows them at once */
|
|
47
|
+
useEffect(() => { sourcesApi.preload(); }, []);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const onPop = () => setPath(location.pathname);
|
|
51
|
+
window.addEventListener('popstate', onPop);
|
|
52
|
+
/* `openfilm open` while this page is open: show the project it opened (with tabs, the app says which tab does) */
|
|
53
|
+
const stop = listen('events', (event) => {
|
|
54
|
+
if (event.type !== 'open') return;
|
|
55
|
+
if (tabs) tabs.openRequest(event.to); else go(event.to);
|
|
56
|
+
}, { watch: true });
|
|
57
|
+
return () => { window.removeEventListener('popstate', onPop); stop(); };
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
/** Show a project: here, or (with tabs) in its own tab. */
|
|
61
|
+
const open = (projectId: string) => { if (tabs) tabs.open(projectId); else go(`/projects/${projectId}`); };
|
|
62
|
+
/** The project shown was deleted: its tab closes (with tabs), or back to the last project. */
|
|
63
|
+
const leave = () => { if (tabs) tabs.report({ gone: true }); else go('/', true); };
|
|
64
|
+
|
|
65
|
+
const id = projectIn(path);
|
|
66
|
+
/* no project in the address: the one opened last that is still there, else the Projects window (`none`); with
|
|
67
|
+
tabs, the projects (no tab is open) */
|
|
68
|
+
const [none, setNone] = useState(false);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (id || tabs) return;
|
|
71
|
+
let alive = true;
|
|
72
|
+
/* the list not read (refused, or no answer in time): the Projects window, which says so and tries again */
|
|
73
|
+
void within(api.projects(), LIST_WAIT_MS).then((projects) => {
|
|
74
|
+
if (!alive) return;
|
|
75
|
+
const last = projects.find((p) => !p.missing);
|
|
76
|
+
if (last) go(`/projects/${last.id}`, true);
|
|
77
|
+
else setNone(true);
|
|
78
|
+
}, () => { if (alive) setNone(true); });
|
|
79
|
+
return () => { alive = false; setNone(false); };
|
|
80
|
+
}, [id]);
|
|
81
|
+
|
|
82
|
+
const page = id ? <ProjectView key={id} projectId={id} onProject={open} onLeave={leave} />
|
|
83
|
+
: tabs ? <Launcher onOpen={open} />
|
|
84
|
+
: !none ? <BrandLoadingScreen labelKey="projects.opening" />
|
|
85
|
+
: (
|
|
86
|
+
<div className="workspace-shell bg-[var(--dock-shell)]">
|
|
87
|
+
<ProjectsPopover open currentProjectId="" onOpen={(next) => go(`/projects/${next}`)} onCurrentRemoved={() => {}} />
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
return <>{page}<ConnectionNotice /><WhatsNew /></>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Studio answers, but not this browser: it was opened at Studio's address without the key (pasted, typed, an agent's
|
|
95
|
+
* browser given the bare address). Nothing to wait for: the page is covered and says which address lets it in.
|
|
96
|
+
*/
|
|
97
|
+
function LockedNotice() {
|
|
98
|
+
const t = useT();
|
|
99
|
+
const [copied, setCopied] = useState(false);
|
|
100
|
+
const COMMAND = 'openfilm open';
|
|
101
|
+
return (
|
|
102
|
+
<div role="alert" className="fixed inset-0 z-[10050] flex items-center justify-center bg-[var(--dock-shell)] p-6">
|
|
103
|
+
<div className="w-full max-w-[460px] rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-6 shadow-[var(--shadow-lg)]">
|
|
104
|
+
<p className="text-[16px] font-semibold text-[var(--text)]">{t('connection.lockedTitle')}</p>
|
|
105
|
+
<p className="mt-2 text-[13px] leading-relaxed text-[var(--text-muted)]">{t('connection.lockedBody')}</p>
|
|
106
|
+
<p className="mt-3 rounded-lg bg-[var(--surface-2)] px-3 py-2 font-mono text-[12px] text-[var(--text-dim)]">{`${location.origin}/open?key=…`}</p>
|
|
107
|
+
{studioHost ? null : (
|
|
108
|
+
<div className="mt-4 flex items-center gap-2 text-[12.5px] text-[var(--text-muted)]">
|
|
109
|
+
<span>{t('connection.lockedHow')}</span>
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
onClick={() => { void navigator.clipboard?.writeText(COMMAND).then(() => setCopied(true), () => {}); }}
|
|
113
|
+
className="rounded-md bg-[var(--surface-2)] px-2 py-0.5 font-mono text-[12px] text-[var(--text)] ring-1 ring-inset ring-[var(--border)] hover:bg-[var(--bg-hover)]"
|
|
114
|
+
>
|
|
115
|
+
{copied ? t('connection.copied') : COMMAND}
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Studio stopped (an update, a crash, the computer slept): a bar across the top while it is away, so nothing seems to
|
|
126
|
+
* hang for no reason. Reconnecting is tried all along; once it has been away a while the page is covered (an export or
|
|
127
|
+
* an edit made now would go nowhere) and the bar says how to start it again: this page reconnects by itself.
|
|
128
|
+
*/
|
|
129
|
+
function ConnectionNotice() {
|
|
130
|
+
const t = useT();
|
|
131
|
+
const state = useSyncExternalStore(studioConnection.subscribe, studioConnection.get);
|
|
132
|
+
const [copied, setCopied] = useState(false);
|
|
133
|
+
useEffect(() => { if (state === 'up') setCopied(false); }, [state]);
|
|
134
|
+
if (state === 'up') return null;
|
|
135
|
+
if (state === 'locked') return <LockedNotice />;
|
|
136
|
+
const gone = state === 'gone';
|
|
137
|
+
const COMMAND = 'openfilm open';
|
|
138
|
+
return (
|
|
139
|
+
<>
|
|
140
|
+
{gone ? <div aria-hidden className="fixed inset-0 z-[10049] bg-black/40 backdrop-blur-[1.5px]" /> : null}
|
|
141
|
+
<div role="alert" className={`fixed inset-x-0 top-0 z-[10050] flex flex-wrap items-center justify-center gap-x-3 gap-y-1.5 px-4 py-2.5 text-[13px] font-medium text-white shadow-lg ${gone ? 'bg-[#b42318]' : 'bg-[#b54708]'}`}>
|
|
142
|
+
<span aria-hidden className={`h-2 w-2 shrink-0 rounded-full bg-white ${gone ? '' : 'animate-pulse'}`} />
|
|
143
|
+
<span>{t(gone ? (studioHost ? 'connection.goneApp' : 'connection.gone') : 'connection.lost')}</span>
|
|
144
|
+
{gone && !studioHost ? (
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
onClick={() => { void navigator.clipboard?.writeText(COMMAND).then(() => setCopied(true), () => {}); }}
|
|
148
|
+
className="rounded-md bg-white/15 px-2 py-0.5 font-mono text-[12.5px] ring-1 ring-inset ring-white/30 hover:bg-white/25"
|
|
149
|
+
>
|
|
150
|
+
{copied ? t('connection.copied') : COMMAND}
|
|
151
|
+
</button>
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio's API, as the editor calls it (studio/server/server.mjs). Same origin, so the launch key travels as the
|
|
3
|
+
* cookie `/open` set; writes are refused from any other origin.
|
|
4
|
+
*/
|
|
5
|
+
import type { ExportJob } from '@/lib/export-jobs';
|
|
6
|
+
import { studioHost } from '@/lib/host';
|
|
7
|
+
import { watchStudioVersion } from '@/lib/studio-version';
|
|
8
|
+
|
|
9
|
+
/** `working`: an agent of the app's chat is working in it right now (it cannot be deleted meanwhile). */
|
|
10
|
+
/** GET /api/version: this Studio's, and npm's latest (null when never learnt; `checking` false: never asked). */
|
|
11
|
+
export interface StudioVersion { version: string; latest: string | null; newer: boolean; checkedAt: number | null; checking: boolean; source: boolean;
|
|
12
|
+
/** the update Studio runs from the top bar (lib/web-update), or one that failed */
|
|
13
|
+
update?: { status: 'updating' | 'failed'; version: string; error?: string } }
|
|
14
|
+
|
|
15
|
+
/** `cover`: when the person set a cover of its own (studio/server/cover.mjs), absent when it has none. */
|
|
16
|
+
export type Project = { id: string; path: string; name: string; openedAt: number; missing: boolean; working?: boolean; cover?: number };
|
|
17
|
+
export type Clip = { src: string; id: string; at?: number; time?: [number] | [number, number];
|
|
18
|
+
box?: { x: number; y: number; w?: number; h?: number; r?: number }; volume?: number; speed?: number; overrides?: unknown[];
|
|
19
|
+
/** a page that sets no size of its own: the px it is laid out at (the stage's without it) */
|
|
20
|
+
size?: { w: number; h: number };
|
|
21
|
+
/** a video's sound alone (an <audio> of its file) */
|
|
22
|
+
sound?: true;
|
|
23
|
+
/** its look: class names for the film's styles, and its own CSS */
|
|
24
|
+
class?: string; style?: string; attrs?: Record<string, string> };
|
|
25
|
+
export type Track = { clips: Clip[]; locked?: boolean; hidden?: boolean; muted?: boolean; attrs?: Record<string, string> };
|
|
26
|
+
export type Film = { stage?: { w: number; h: number }; tracks: Track[] };
|
|
27
|
+
/** `value`: film.html as read, every clip named (a clip with no id takes its file's name, as the server and preview do). */
|
|
28
|
+
export type ProjectFilm = { project: Project; folder: string; text: string; rev: string; value: Film | null; doc: Film | null; problems: string[] };
|
|
29
|
+
export type MediaFile = { path: string; kind: 'video' | 'image' | 'audio' | 'page' | 'other'; size: number; mtime: number; added?: number };
|
|
30
|
+
/** A project's editor settings (studio/server/project-settings.mjs). */
|
|
31
|
+
export type ProjectSettings = { fps?: number; trackNames?: { name: string; index: number; clips: string[] }[]; trackHeights?: { height: number; index: number; clips: string[] }[] };
|
|
32
|
+
export type Listing = { files: MediaFile[]; folders: string[]; pages: { path: string; size: number; mtime: number }[] };
|
|
33
|
+
export type Op =
|
|
34
|
+
| { op: 'set'; clip: string; field: string; value?: unknown }
|
|
35
|
+
| { op: 'move'; clip: string; at: number; track?: number; newTrack?: number }
|
|
36
|
+
| { op: 'insert'; clip?: Partial<Clip> & { src: string }; from?: string; at?: number; after?: string; track?: number | { insert: number }; newTrack?: number; link?: string | null }
|
|
37
|
+
| { op: 'remove'; clip: string }
|
|
38
|
+
| { op: 'split'; clip: string; at: number; time: [number, number]; restTime: [number, number] }
|
|
39
|
+
| { op: 'split'; clip: string; left: Record<string, PropValue>; right: Record<string, PropValue>; rightId?: string }
|
|
40
|
+
| { op: 'track'; track: number; field: 'locked' | 'hidden' | 'muted'; value: boolean }
|
|
41
|
+
| { op: 'reorder'; from: number; to: number }
|
|
42
|
+
| { op: 'props'; edits: PropEdit[] }
|
|
43
|
+
| { op: 'stage'; w: number; h: number }
|
|
44
|
+
| { op: 'fields'; clip: string; fields: Record<string, unknown> };
|
|
45
|
+
|
|
46
|
+
/** A value a clip's property takes (null removes it). */
|
|
47
|
+
export type PropValue = number | string | boolean | readonly number[] | Record<string, unknown> | null;
|
|
48
|
+
/** One property of one clip, by the clip's id (see studio/server/ops.mjs). */
|
|
49
|
+
export type PropEdit = { clip: string; prop: string; value: PropValue };
|
|
50
|
+
|
|
51
|
+
export class ApiError extends Error {
|
|
52
|
+
constructor(message: string, readonly status: number, readonly body: Record<string, unknown>) {
|
|
53
|
+
super(message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Writes on their way (edits, renames…): a reload for a new Studio waits for them (studioVersion). */
|
|
58
|
+
let writing = 0;
|
|
59
|
+
|
|
60
|
+
async function call<T>(path: string, init: RequestInit & { json?: unknown } = {}): Promise<T> {
|
|
61
|
+
const { json, ...rest } = init;
|
|
62
|
+
const write = (rest.method ?? 'GET') !== 'GET';
|
|
63
|
+
if (write) writing += 1;
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(`/api/${path}`, {
|
|
66
|
+
...rest,
|
|
67
|
+
headers: json === undefined ? rest.headers : { 'content-type': 'application/json', ...rest.headers },
|
|
68
|
+
body: json === undefined ? rest.body : JSON.stringify(json),
|
|
69
|
+
});
|
|
70
|
+
const body = await res.json().catch(() => ({}));
|
|
71
|
+
if (res.status === 401) markLocked();
|
|
72
|
+
if (!res.ok) throw new ApiError(String(body.error ?? `Studio answered ${res.status}`), res.status, body);
|
|
73
|
+
return body as T;
|
|
74
|
+
} finally {
|
|
75
|
+
if (write) writing -= 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const p = (id: string) => `projects/${encodeURIComponent(id)}`;
|
|
80
|
+
|
|
81
|
+
/** Studio's own preferences, the same for every project (studio/server/prefs.mjs). */
|
|
82
|
+
export interface StudioPrefs {
|
|
83
|
+
proxies: { on: boolean; height: 540 | 720 | 1080 };
|
|
84
|
+
seenVersion: string | null;
|
|
85
|
+
projectsFolder: string | null;
|
|
86
|
+
/** the keyboard shortcuts: the scheme in use and the custom ones (lib/keymap `KeyPrefs`, checked there) */
|
|
87
|
+
keys?: unknown;
|
|
88
|
+
}
|
|
89
|
+
/** Where the projects library is (src/locations.mjs): the folder, its default, whether the person chose it, the free space on its disk, how many projects are in it. */
|
|
90
|
+
export interface StudioLocation { path: string; default: string; chosen: boolean; free: number | null; count: number }
|
|
91
|
+
export interface StudioLocations { projects: StudioLocation }
|
|
92
|
+
/** Said on the window when they change, for what plays by them (the preview's proxies). */
|
|
93
|
+
export const PREFS_EVENT = 'openfilm:prefs';
|
|
94
|
+
/** Said on the window when the library moved, for the Projects list. */
|
|
95
|
+
export const LIBRARY_EVENT = 'openfilm:library';
|
|
96
|
+
|
|
97
|
+
/** A problem as Studio keeps it (src/problems.mjs): when, what it was about, what it said. */
|
|
98
|
+
export interface StudioProblem {
|
|
99
|
+
t: string;
|
|
100
|
+
area: string;
|
|
101
|
+
what: string;
|
|
102
|
+
agent?: string;
|
|
103
|
+
step?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const api = {
|
|
107
|
+
/** this Studio's version and the latest openfilm on npm; `check`: ask npm now */
|
|
108
|
+
version: (check = false) => call<StudioVersion>(`version${check ? '?check=1' : ''}`),
|
|
109
|
+
/** the newest openfilm, run in the background (`openfilm update`): Studio starts again on it; refused (ApiError, `body.code`) while busy */
|
|
110
|
+
update: () => call<{ update: NonNullable<StudioVersion['update']> }>('update', { method: 'POST', json: {} }),
|
|
111
|
+
/** `openfilm doctor`, run by this Studio: the report to paste into an issue (src/doctor.mjs) */
|
|
112
|
+
doctor: () => call<{ ok: boolean; text: string }>('doctor'),
|
|
113
|
+
/** what went wrong in the last two weeks, newest first (src/problems.mjs) */
|
|
114
|
+
problems: () => call<{ problems: StudioProblem[] }>('diagnostics/problems').then((r) => r.problems),
|
|
115
|
+
/** the diagnostic file (server/diagnostics.mjs): `report` and the rest, zipped into the exports folder and shown there */
|
|
116
|
+
exportDiagnostics: (report: string, conversations: boolean) => call<{ path: string; files: string[]; conversations: boolean }>('diagnostics/export', { method: 'POST', json: { report, conversations } }),
|
|
117
|
+
projects: () => call<{ projects: Project[] }>('projects').then((r) => r.projects),
|
|
118
|
+
prefs: () => call<StudioPrefs>('prefs'),
|
|
119
|
+
/** where new projects and exports go, and the free space there */
|
|
120
|
+
locations: () => call<StudioLocations>('locations'),
|
|
121
|
+
/** move the library to `path` (null: the default); with `move`, its projects go too (those that could not are in `failed`) */
|
|
122
|
+
moveLibrary: (path: string | null, move: boolean) => call<{ moved: number; failed: { name: string; error: string }[]; locations: StudioLocations }>('locations/projects', { method: 'PUT', json: { path, move } }).then((done) => {
|
|
123
|
+
window.dispatchEvent(new Event(LIBRARY_EVENT));
|
|
124
|
+
return done;
|
|
125
|
+
}),
|
|
126
|
+
/** a project's folder, shown in the system's file manager */
|
|
127
|
+
revealProject: (id: string) => call<object>(`projects/${encodeURIComponent(id)}/reveal`, { method: 'POST', json: {} }),
|
|
128
|
+
/** one of them in the system's file manager */
|
|
129
|
+
openLocation: (which: keyof StudioLocations) => call<{ ok: true }>(`locations/${which}`, { method: 'POST', json: {} }),
|
|
130
|
+
setPrefs: (patch: { proxies?: Partial<StudioPrefs['proxies']>; seenVersion?: string; projectsFolder?: string | null; keys?: unknown }) => call<StudioPrefs>('prefs', { method: 'PATCH', json: patch }).then((prefs) => {
|
|
131
|
+
window.dispatchEvent(new Event(PREFS_EVENT));
|
|
132
|
+
return prefs;
|
|
133
|
+
}),
|
|
134
|
+
/** the system's folder dialog, opened by Studio (a page in a browser cannot learn a path): the folder, or null when
|
|
135
|
+
cancelled; throws (501) where this system has none */
|
|
136
|
+
pickFolder: (title: string, at?: string) => call<{ path: string | null }>('pick-folder', { method: 'POST', json: { title, ...(at ? { at } : {}) } }).then((r) => r.path),
|
|
137
|
+
newProject: (name?: string) => call<{ project: Project }>('projects', { method: 'POST', json: { name } }).then((r) => r.project),
|
|
138
|
+
/** open a folder (its full path) as a project: made one when it is not yet */
|
|
139
|
+
openFolder: (path: string) => call<{ project: Project }>('projects', { method: 'POST', json: { path } }).then((r) => r.project),
|
|
140
|
+
rename: (id: string, name: string) => call<{ project: Project }>(p(id), { method: 'PATCH', json: { name } }).then((r) => {
|
|
141
|
+
studioHost?.projectChanged?.(r.project);
|
|
142
|
+
return r.project;
|
|
143
|
+
}),
|
|
144
|
+
forget: (id: string) => call<object>(p(id), { method: 'DELETE' }),
|
|
145
|
+
film: (id: string) => call<ProjectFilm>(p(id)),
|
|
146
|
+
/** the person opened it here: the one `openfilm open` alone comes back to */
|
|
147
|
+
opened: (id: string) => call<{ project: Project }>(`${p(id)}/opened`, { method: 'POST' }),
|
|
148
|
+
edit: (id: string, base: string | null, ops: Op[]) => call<{ text: string; rev: string; value: Film; doc: Film; rebased: boolean }>(`${p(id)}/edit`, { method: 'POST', json: { base, ops } }),
|
|
149
|
+
files: (id: string) => call<Listing>(`${p(id)}/files`),
|
|
150
|
+
/** the editor's settings of the project (`.film/settings.json`): its frame rate, its tracks' names */
|
|
151
|
+
settings: (id: string) => call<{ settings: ProjectSettings }>(`${p(id)}/settings`).then((r) => r.settings),
|
|
152
|
+
/** change some of them (null takes one away); answers them all as kept */
|
|
153
|
+
patchSettings: (id: string, patch: { fps?: number | null; trackNames?: ProjectSettings['trackNames'] | null; trackHeights?: ProjectSettings['trackHeights'] | null }) => (
|
|
154
|
+
call<{ settings: ProjectSettings }>(`${p(id)}/settings`, { method: 'PATCH', json: patch }).then((r) => r.settings)
|
|
155
|
+
),
|
|
156
|
+
/** its markers (`.film/markers.json`, lib/markers): the whole list, read and written */
|
|
157
|
+
markers: (id: string) => call<{ markers: unknown }>(`${p(id)}/markers`).then((r) => r.markers),
|
|
158
|
+
putMarkers: (id: string, markers: unknown[]) => call<{ markers: unknown }>(`${p(id)}/markers`, { method: 'PUT', json: { markers } }).then((r) => r.markers),
|
|
159
|
+
/** the loudness of a part of a file's sound (its own seconds): integrated LUFS (null: silent) and true peak */
|
|
160
|
+
loudness: (id: string, path: string, from: number, to: number) => call<{ lufs: number | null; peak: number | null }>(
|
|
161
|
+
`${p(id)}/media?what=loudness&path=${encodeURIComponent(path)}&from=${from}&to=${to}`,
|
|
162
|
+
),
|
|
163
|
+
/** a video's frame (`ms` of its own time) made a still beside it; answers its path */
|
|
164
|
+
freezeFrame: (id: string, path: string, ms: number) => call<{ path: string }>(`${p(id)}/files/still`, { method: 'POST', json: { path, ms } }).then((r) => r.path),
|
|
165
|
+
/** The project's own cover for its card (studio/server/cover.mjs): a picture from this computer, a picture of the project,
|
|
166
|
+
the film at `ms`, or none (null: its film's poster again). Each answers when it was set. */
|
|
167
|
+
setCover: (id: string, from: { file: Blob } | { path: string } | { ms: number }) => call<{ cover: number | null }>(`${p(id)}/cover`,
|
|
168
|
+
'file' in from ? { method: 'PUT', body: from.file, headers: { 'content-type': 'application/octet-stream' } } : { method: 'POST', json: from }).then((r) => r.cover),
|
|
169
|
+
clearCover: (id: string) => call<{ cover: null }>(`${p(id)}/cover`, { method: 'DELETE' }).then(() => null),
|
|
170
|
+
answerAsk: (askId: string, allow: boolean) => call<{ answered: boolean }>(`get/asks/${encodeURIComponent(askId)}`, { method: 'POST', json: { allow } }),
|
|
171
|
+
/** the provider's own page for its balance, opened in the person's browser */
|
|
172
|
+
openBilling: (providerId: string) => call<Record<string, never>>(`providers/${encodeURIComponent(providerId)}/billing`, { method: 'POST' }),
|
|
173
|
+
/** clips (by id), clips together (a group) and files (by path) of a project put in Saved (studio/server/saved.mjs):
|
|
174
|
+
each one's place there */
|
|
175
|
+
save: (id: string, what: { clips?: SavedAsk[]; groups?: Array<SavedAsk & { ids: string[] }>; files?: Array<SavedAsk & { path: string }>; into?: string }) => (
|
|
176
|
+
call<{ paths: string[] }>(`${p(id)}/saved`, { method: 'POST', json: what }).then((r) => r.paths)
|
|
177
|
+
),
|
|
178
|
+
/** a thing of Saved (its `saved/` path) used in the project: a file imported (its place), or a bundle's files brought in
|
|
179
|
+
and the clip, a group's clips or the page to put on the timeline */
|
|
180
|
+
takeSaved: (id: string, path: string) => call<{ clip: Record<string, unknown> & { src: string } } | { clips: SavedGroupClip[] } | { path: string }>(
|
|
181
|
+
`${p(id)}/saved/take`, { method: 'POST', json: { path } },
|
|
182
|
+
),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** A clip of a saved group: its track counted from the group's top one, its time in seconds from the group's start. */
|
|
186
|
+
export type SavedGroupClip = { track: number; at: number; clip: Record<string, unknown> & { src: string } };
|
|
187
|
+
|
|
188
|
+
/** What is said of a thing put in Saved: a clip's id (a file's path beside it), a name, and how long and big it is. */
|
|
189
|
+
export type SavedAsk = { id?: string; name?: string; durationMs?: number; w?: number; h?: number };
|
|
190
|
+
|
|
191
|
+
/** What an agent's `openfilm get` asks the person before it spends (studio/server/get.mjs). */
|
|
192
|
+
export type SpendAsk = {
|
|
193
|
+
id: string; kind: 'video'; provider: string; model?: string; seconds?: number; prompt?: string;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** What the person is told without a question (studio/server/get.mjs): a provider with no balance left for a run. */
|
|
197
|
+
export type StudioNotice = { id: string; kind: 'balance'; provider: string; providerId: string };
|
|
198
|
+
|
|
199
|
+
export type FolderEvent = { type: 'film'; rev: string } | { type: 'files'; paths?: string[] } | { type: 'history' } | { type: 'export'; job: ExportJob } | { type: 'open'; to: string }
|
|
200
|
+
| { type: 'history-progress'; op: string; phase: string; done: number; total: number }
|
|
201
|
+
| { type: 'ask'; ask: SpendAsk } | { type: 'asked'; id: string } | { type: 'notice'; notice: StudioNotice } | { type: 'project'; project: Project }
|
|
202
|
+
/* the project was deleted, or its folder is not there any more: its pages leave it */
|
|
203
|
+
| { type: 'gone' };
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Listen to a stream of events (a WebSocket: a browser's six HTTP connections to Studio stay free for the page's
|
|
207
|
+
* requests, however many pages are open); returns a function that stops listening. It reconnects on its own;
|
|
208
|
+
* `onBack` runs when it does (Studio stopped and started again: what changed meanwhile was not told). `watch`: this
|
|
209
|
+
* stream says whether Studio answers (studioConnection), and reloads the page when Studio comes back as another
|
|
210
|
+
* version (lib/studio-version); the page's own one does. It also tells Studio whether the page is in sight, so
|
|
211
|
+
* `openfilm open` opens a browser when no page is (a tab behind others, a minimized window).
|
|
212
|
+
*/
|
|
213
|
+
export function listen(path: string, on: (event: FolderEvent) => void, { onBack, watch = false, gone }: { onBack?: () => void; watch?: boolean; gone?: () => Promise<boolean> } = {}): () => void {
|
|
214
|
+
const inSight = () => document.visibilityState === 'visible';
|
|
215
|
+
const url = () => `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/api/${path}${watch ? `?visible=${inSight() ? 1 : 0}` : ''}`;
|
|
216
|
+
const tell = () => { if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ visible: inSight() })); };
|
|
217
|
+
if (watch) document.addEventListener('visibilitychange', tell);
|
|
218
|
+
let socket: WebSocket | null = null;
|
|
219
|
+
let stopped = false, lost = false, tries = 0;
|
|
220
|
+
let again: ReturnType<typeof setTimeout> | null = null;
|
|
221
|
+
const watcher = watch ? watchConnection() : null;
|
|
222
|
+
if (watch) studioVersion ??= watchStudioVersion({
|
|
223
|
+
version: () => fetch('/api/health', { cache: 'no-store' }).then((r) => r.json()).then((h) => (typeof h?.version === 'string' ? h.version : null)),
|
|
224
|
+
idle: () => writing === 0,
|
|
225
|
+
reload: () => location.reload(),
|
|
226
|
+
storage: (() => { try { return sessionStorage; } catch { return null; } })(),
|
|
227
|
+
});
|
|
228
|
+
const connect = () => {
|
|
229
|
+
const s = new WebSocket(url());
|
|
230
|
+
socket = s;
|
|
231
|
+
s.onopen = () => {
|
|
232
|
+
tries = 0;
|
|
233
|
+
watcher?.up();
|
|
234
|
+
if (lost) {
|
|
235
|
+
lost = false;
|
|
236
|
+
onBack?.();
|
|
237
|
+
/* back as another version (a newer openfilm, or the desktop app's): this page's code is reloaded to match */
|
|
238
|
+
if (watch) void studioVersion?.back();
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
s.onmessage = (e) => { try { on(JSON.parse(String(e.data)) as FolderEvent); } catch { /* not ours */ } };
|
|
242
|
+
s.onclose = () => {
|
|
243
|
+
if (stopped) return;
|
|
244
|
+
lost = true;
|
|
245
|
+
watcher?.down();
|
|
246
|
+
/* refused, not gone: Studio is there but this browser has no key (the stream says no reason, a request does) */
|
|
247
|
+
if (watch && tries === 0) void fetch('/api/version', { cache: 'no-store' }).then((r) => { if (r.status === 401) markLocked(); }, () => {});
|
|
248
|
+
/* soon at first, then every second: Studio restarting (`openfilm open` starts it afresh) is back within one
|
|
249
|
+
or two, and waits a few for the pages before it */
|
|
250
|
+
again = setTimeout(connect, Math.min(1000, 250 * 2 ** tries++));
|
|
251
|
+
/* refused again and again while Studio answers: what it streams may be gone (a project deleted, its folder
|
|
252
|
+
moved away), which reconnecting can't change; asked, and if so said once and left */
|
|
253
|
+
if (gone && (tries === 3 || tries % 20 === 0)) {
|
|
254
|
+
void gone().then((yes) => {
|
|
255
|
+
if (!yes || stopped) return;
|
|
256
|
+
stopped = true;
|
|
257
|
+
if (again) clearTimeout(again);
|
|
258
|
+
on({ type: 'gone' });
|
|
259
|
+
}, () => {});
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
connect();
|
|
264
|
+
return () => { stopped = true; if (again) clearTimeout(again); if (watch) document.removeEventListener('visibilitychange', tell); watcher?.stop(); socket?.close(); };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Which version of Studio served this page (see listen's `watch`). */
|
|
268
|
+
let studioVersion: ReturnType<typeof watchStudioVersion> | null = null;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Whether Studio answers, for the notice when it does not: 'up', 'lost' (stopped a moment ago, reconnecting), 'gone'
|
|
272
|
+
* (still not back), or 'locked': Studio answers but not this browser, which was never let in (its address was typed
|
|
273
|
+
* or pasted without the key `openfilm open` prints, or its key is another launch's). Waiting does not fix that one.
|
|
274
|
+
*/
|
|
275
|
+
export type Connection = 'up' | 'lost' | 'gone' | 'locked';
|
|
276
|
+
let connection: Connection = 'up';
|
|
277
|
+
const connectionListeners = new Set<() => void>();
|
|
278
|
+
export const studioConnection = {
|
|
279
|
+
get: () => connection,
|
|
280
|
+
subscribe(fn: () => void) { connectionListeners.add(fn); return () => { connectionListeners.delete(fn); }; },
|
|
281
|
+
};
|
|
282
|
+
/** Studio answered this page 401: it was never let in (see Connection). */
|
|
283
|
+
export function markLocked() { setConnection('locked'); }
|
|
284
|
+
function setConnection(next: Connection) {
|
|
285
|
+
if (next === connection || (connection === 'locked' && next !== 'up')) return;
|
|
286
|
+
connection = next;
|
|
287
|
+
for (const fn of [...connectionListeners]) fn();
|
|
288
|
+
}
|
|
289
|
+
/** A moment without Studio is said, a long one more plainly. */
|
|
290
|
+
function watchConnection() {
|
|
291
|
+
let timers: ReturnType<typeof setTimeout>[] = [];
|
|
292
|
+
const clear = () => { for (const timer of timers) clearTimeout(timer); timers = []; };
|
|
293
|
+
return {
|
|
294
|
+
down() {
|
|
295
|
+
if (timers.length || connection !== 'up') return;
|
|
296
|
+
/* a blip (the computer woke, the stream was cut and came straight back) is not worth a notice */
|
|
297
|
+
timers = [setTimeout(() => setConnection('lost'), 2000), setTimeout(() => setConnection('gone'), 20_000)];
|
|
298
|
+
},
|
|
299
|
+
up() { clear(); setConnection('up'); },
|
|
300
|
+
stop: clear,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/* One stream per project, shared by everyone listening on the page. */
|
|
304
|
+
const streams = new Map<string, { stop: () => void; listeners: Set<(event: FolderEvent) => void> }>();
|
|
305
|
+
export function projectEvents(id: string, on: (event: FolderEvent) => void): () => void {
|
|
306
|
+
let stream = streams.get(id);
|
|
307
|
+
if (!stream) {
|
|
308
|
+
const listeners = new Set<(event: FolderEvent) => void>();
|
|
309
|
+
const tell = (event: FolderEvent) => {
|
|
310
|
+
if (listeners.size && event.type === 'project') studioHost?.projectChanged?.(event.project);
|
|
311
|
+
for (const fn of [...listeners]) fn(event);
|
|
312
|
+
};
|
|
313
|
+
/* back after Studio was away: the film, the files and the history may all have changed meanwhile */
|
|
314
|
+
const stop = listen(`${p(id)}/events`, tell, {
|
|
315
|
+
onBack: () => { tell({ type: 'film', rev: '' }); tell({ type: 'files' }); tell({ type: 'history' }); },
|
|
316
|
+
/* the project off the list, or its folder missing: gone (a list that can't be read says nothing) */
|
|
317
|
+
gone: () => call<{ projects: Project[] }>('projects').then((r) => { const found = r.projects.find((x) => x.id === id); return !found || Boolean(found.missing); }),
|
|
318
|
+
});
|
|
319
|
+
stream = { stop, listeners };
|
|
320
|
+
streams.set(id, stream);
|
|
321
|
+
}
|
|
322
|
+
stream.listeners.add(on);
|
|
323
|
+
const mine = stream;
|
|
324
|
+
return () => {
|
|
325
|
+
mine.listeners.delete(on);
|
|
326
|
+
if (!mine.listeners.size && streams.get(id) === mine) { mine.stop(); streams.delete(id); }
|
|
327
|
+
};
|
|
328
|
+
}
|