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,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folders of the Projects popover: a way to group the list, kept by Studio beside the list itself (nothing moves on
|
|
3
|
+
* disk). A project is in at most one (`folderId`); the top level shows the projects in none.
|
|
4
|
+
*
|
|
5
|
+
* GET / POST { name } /api/projects/folders → { folders } / { folder }
|
|
6
|
+
* PATCH { name } /api/projects/folders/:id → { folder }; DELETE /api/projects/folders/:id → { projects } (their ids)
|
|
7
|
+
* PUT { folderId } /api/projects/:id/folder (null: no folder)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** `parentId`: the folder it is in (none, or one that is gone: the top level). */
|
|
11
|
+
export type ProjectFolder = { id: string; name: string; createdAt: number; parentId?: string };
|
|
12
|
+
|
|
13
|
+
/** What of a project the list sorts, files and searches by. */
|
|
14
|
+
export type ListedProject = {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
openedAt: number;
|
|
18
|
+
createdAt?: number;
|
|
19
|
+
/** the film's length in seconds, when known */
|
|
20
|
+
duration?: number;
|
|
21
|
+
folderId?: string;
|
|
22
|
+
poster?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type LibrarySort = 'recent' | 'newest' | 'oldest' | 'duration' | 'title';
|
|
26
|
+
|
|
27
|
+
async function call<T>(path: string, method = 'GET', json?: unknown): Promise<T> {
|
|
28
|
+
const res = await fetch(`/api/projects/${path}`, {
|
|
29
|
+
method,
|
|
30
|
+
...(json === undefined ? {} : { headers: { 'content-type': 'application/json' }, body: JSON.stringify(json) }),
|
|
31
|
+
});
|
|
32
|
+
const body = await res.json().catch(() => ({}));
|
|
33
|
+
if (!res.ok) throw new Error(String(body.error ?? `Studio answered ${res.status}`));
|
|
34
|
+
return body as T;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const folderApi = {
|
|
38
|
+
list: () => call<{ folders: ProjectFolder[] }>('folders').then((r) => r.folders),
|
|
39
|
+
create: (name: string, parentId: string | null = null) => call<{ folder: ProjectFolder }>('folders', 'POST', { name, ...(parentId ? { parentId } : {}) }).then((r) => r.folder),
|
|
40
|
+
rename: (id: string, name: string) => call<{ folder: ProjectFolder }>(`folders/${encodeURIComponent(id)}`, 'PATCH', { name }).then((r) => r.folder),
|
|
41
|
+
/** takes the folder off the list; resolves to the ids of the projects that were in it */
|
|
42
|
+
remove: (id: string) => call<{ projects: string[] }>(`folders/${encodeURIComponent(id)}`, 'DELETE').then((r) => r.projects),
|
|
43
|
+
move: (projectId: string, folderId: string | null) => call<object>(`${encodeURIComponent(projectId)}/folder`, 'PUT', { folderId }),
|
|
44
|
+
/** a folder into another (null: the top level), with what it holds */
|
|
45
|
+
moveFolder: (id: string, parentId: string | null) => call<{ folder: ProjectFolder }>(`folders/${encodeURIComponent(id)}`, 'PATCH', { parentId }).then((r) => r.folder),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What the list starts with: "New project" wherever projects are listed, the top level or a folder (not over a
|
|
50
|
+
* search's results); the examples at the top level only.
|
|
51
|
+
*/
|
|
52
|
+
export function listStarts({ folderId, query }: { folderId: string | null; query: string }): { newProject: boolean; examples: boolean } {
|
|
53
|
+
const listing = !query.trim();
|
|
54
|
+
return { newProject: listing, examples: listing && folderId === null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A new project inside `folderId` (null: the top level): made, then filed there before it opens. One that cannot be
|
|
59
|
+
* filed (its folder went meanwhile) is still made, at the top level.
|
|
60
|
+
*/
|
|
61
|
+
export async function newProjectIn<P extends { id: string }>(
|
|
62
|
+
folderId: string | null,
|
|
63
|
+
{ create, move }: { create: () => Promise<P>; move: (projectId: string, folderId: string | null) => Promise<unknown> },
|
|
64
|
+
): Promise<P> {
|
|
65
|
+
const project = await create();
|
|
66
|
+
if (folderId) await move(project.id, folderId).catch(() => {});
|
|
67
|
+
return project;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The projects to show, sorted. Like a file system: the top level shows the projects in no folder, a folder its own.
|
|
72
|
+
* A search looks everywhere (a person searching wants "where is that film", not to guess its folder first).
|
|
73
|
+
*/
|
|
74
|
+
export function visibleProjects<P extends ListedProject>(
|
|
75
|
+
projects: readonly P[],
|
|
76
|
+
{ query, folderId, sort, titleOf, folders }: {
|
|
77
|
+
query: string;
|
|
78
|
+
folderId: string | null;
|
|
79
|
+
sort: LibrarySort;
|
|
80
|
+
titleOf: (project: P) => string;
|
|
81
|
+
/** the folders there are: a project filed in one that is gone shows at the top level */
|
|
82
|
+
folders: readonly ProjectFolder[];
|
|
83
|
+
},
|
|
84
|
+
): P[] {
|
|
85
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
86
|
+
const known = new Set(folders.map((f) => f.id));
|
|
87
|
+
const placed = (p: P) => (p.folderId && known.has(p.folderId) ? p.folderId : null);
|
|
88
|
+
const created = (p: P) => p.createdAt ?? p.openedAt;
|
|
89
|
+
return projects
|
|
90
|
+
.filter((p) => (needle ? p.name.toLocaleLowerCase().includes(needle) : placed(p) === folderId))
|
|
91
|
+
.sort((a, b) => {
|
|
92
|
+
if (sort === 'recent') return b.openedAt - a.openedAt;
|
|
93
|
+
if (sort === 'oldest') return created(a) - created(b);
|
|
94
|
+
if (sort === 'duration') return (b.duration ?? 0) - (a.duration ?? 0);
|
|
95
|
+
if (sort === 'title') return titleOf(a).localeCompare(titleOf(b));
|
|
96
|
+
return created(b) - created(a);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The folder a folder is in, or null (the top level, or its parent is gone). */
|
|
101
|
+
export function parentOf(folder: ProjectFolder, folders: readonly ProjectFolder[]): string | null {
|
|
102
|
+
return folder.parentId && folders.some((f) => f.id === folder.parentId) ? folder.parentId : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A folder and the folders it is in, the outermost first (the breadcrumb). A loop is cut where it comes back. */
|
|
106
|
+
export function folderTrail(folders: readonly ProjectFolder[], id: string | null): ProjectFolder[] {
|
|
107
|
+
const trail: ProjectFolder[] = [];
|
|
108
|
+
const seen = new Set<string>();
|
|
109
|
+
for (let at = folders.find((f) => f.id === id); at && !seen.has(at.id); at = folders.find((f) => f.id === parentOf(at!, folders))) {
|
|
110
|
+
seen.add(at.id);
|
|
111
|
+
trail.unshift(at);
|
|
112
|
+
}
|
|
113
|
+
return trail;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The folders that hold `id`, itself first: a project in it counts for each of them. */
|
|
117
|
+
const holders = (folders: readonly ProjectFolder[], id: string) => folderTrail(folders, id).map((f) => f.id).reverse();
|
|
118
|
+
|
|
119
|
+
/** How many projects each folder holds, those of the folders inside it too. */
|
|
120
|
+
export function folderCounts(projects: readonly ListedProject[], folders: readonly ProjectFolder[] = []): Record<string, number> {
|
|
121
|
+
const counts: Record<string, number> = {};
|
|
122
|
+
for (const p of projects) {
|
|
123
|
+
if (!p.folderId) continue;
|
|
124
|
+
for (const id of folders.length ? holders(folders, p.folderId) : [p.folderId]) counts[id] = (counts[id] ?? 0) + 1;
|
|
125
|
+
}
|
|
126
|
+
return counts;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The small covers on each folder's card: its newest projects, up to four, each with its poster when it has one (a
|
|
131
|
+
* cover without one shows that project's own face, as its card does).
|
|
132
|
+
*/
|
|
133
|
+
export function folderPosters(projects: readonly ListedProject[], folders: readonly ProjectFolder[] = []): Record<string, { id: string; poster?: string }[]> {
|
|
134
|
+
const out: Record<string, { id: string; poster?: string }[]> = {};
|
|
135
|
+
const newest = [...projects].sort((a, b) => (b.createdAt ?? b.openedAt) - (a.createdAt ?? a.openedAt));
|
|
136
|
+
for (const p of newest) {
|
|
137
|
+
if (!p.folderId) continue;
|
|
138
|
+
for (const id of folders.length ? holders(folders, p.folderId) : [p.folderId]) {
|
|
139
|
+
const list = (out[id] ??= []);
|
|
140
|
+
if (list.length < 4) list.push(p.poster ? { id: p.id, poster: p.poster } : { id: p.id });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A project's version history as the Version history panel shows it, and the calls that change it
|
|
3
|
+
* (studio/server/history.mjs): git, as a person knows it from GitHub — commits on branches, uncommitted changes,
|
|
4
|
+
* switching, merging — told in the film's terms. Every commit is the person's; nothing commits on its own.
|
|
5
|
+
*
|
|
6
|
+
* GET /api/projects/:id/history → { status, commits }
|
|
7
|
+
* GET /api/projects/:id/history/files → { held, ignored }: the film's files, and the others
|
|
8
|
+
* GET /api/projects/:id/history/poster/:commit the film's picture when it was committed
|
|
9
|
+
* POST /api/projects/:id/history/commit { message }
|
|
10
|
+
* POST /api/projects/:id/history/discard
|
|
11
|
+
* POST /api/projects/:id/history/restore { commit } the commit's files, as uncommitted changes
|
|
12
|
+
* POST /api/projects/:id/history/checkout { branch, create?, from?, carry? }
|
|
13
|
+
* PATCH /api/projects/:id/history/branches/:name { name }
|
|
14
|
+
* DELETE /api/projects/:id/history/branches/:name[?force=1]
|
|
15
|
+
* POST /api/projects/:id/history/merge { branch, message, prefer? }
|
|
16
|
+
*
|
|
17
|
+
* While an action runs, the project's events say how far it is ({ type: 'history-progress' }: HistoryProgress).
|
|
18
|
+
* A refusal comes with a `code` ('dirty', 'nothing', 'conflict', 'unmerged', 'exists', 'name', 'current', 'none'),
|
|
19
|
+
* which the panel says in its own words.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface Counts { added: number; removed: number; edited: number }
|
|
23
|
+
/** What changed, as the film tells it (see history.mjs Summary). */
|
|
24
|
+
export interface Summary { scenes: Counts; sounds: Counts; stage: boolean; other: boolean }
|
|
25
|
+
export interface Branch { name: string; commit: string; at: number; message: string; current: boolean; parked: boolean }
|
|
26
|
+
/** The film's files now (what a commit keeps), and how many other files the folder has (never kept). */
|
|
27
|
+
export interface HistoryFiles { held: number; heldBytes: number; ignored: number }
|
|
28
|
+
export interface HistoryStatus {
|
|
29
|
+
branch: string;
|
|
30
|
+
head: string | null;
|
|
31
|
+
branches: Branch[];
|
|
32
|
+
changes: { count: number; summary: Summary } | null;
|
|
33
|
+
files: HistoryFiles | null;
|
|
34
|
+
}
|
|
35
|
+
/** A file, by its path in the project, and its size. */
|
|
36
|
+
export interface HistoryFile { path: string; size: number }
|
|
37
|
+
/** How far an action is: `phase` reading the film's files, keeping media, writing files, or updating the history. */
|
|
38
|
+
export interface HistoryProgress { op: string; phase: 'scan' | 'store' | 'write' | 'migrate'; done: number; total: number }
|
|
39
|
+
export interface Commit { commit: string; parents: string[]; at: number; message: string; body: string; tags: string[]; summary: Summary }
|
|
40
|
+
export interface HistoryState { status: HistoryStatus; commits: Commit[] }
|
|
41
|
+
|
|
42
|
+
const num = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
43
|
+
const counts = (raw: unknown): Counts => {
|
|
44
|
+
const c = (raw ?? {}) as Record<string, unknown>;
|
|
45
|
+
return { added: num(c.added), removed: num(c.removed), edited: num(c.edited) };
|
|
46
|
+
};
|
|
47
|
+
const summaryOf = (raw: unknown): Summary => {
|
|
48
|
+
const s = (raw ?? {}) as Record<string, unknown>;
|
|
49
|
+
return { scenes: counts(s.scenes), sounds: counts(s.sounds), stage: s.stage === true, other: s.other === true };
|
|
50
|
+
};
|
|
51
|
+
const text = (v: unknown) => (typeof v === 'string' ? v : '');
|
|
52
|
+
|
|
53
|
+
/** The server's answer, checked: anything unreadable is dropped rather than drawn as an empty row. */
|
|
54
|
+
export function normalizeHistory(raw: unknown): HistoryState | null {
|
|
55
|
+
const body = raw as { status?: Record<string, unknown>; commits?: unknown } | null;
|
|
56
|
+
const s = body?.status;
|
|
57
|
+
if (!s || typeof s.branch !== 'string') return null;
|
|
58
|
+
const branches: Branch[] = (Array.isArray(s.branches) ? s.branches : []).flatMap((b) => {
|
|
59
|
+
const r = b as Record<string, unknown> | null;
|
|
60
|
+
return r && typeof r.name === 'string' && r.name
|
|
61
|
+
? [{ name: r.name, commit: text(r.commit), at: num(r.at), message: text(r.message), current: r.current === true, parked: r.parked === true }]
|
|
62
|
+
: [];
|
|
63
|
+
});
|
|
64
|
+
const changes = s.changes && typeof s.changes === 'object'
|
|
65
|
+
? { count: num((s.changes as Record<string, unknown>).count), summary: summaryOf((s.changes as Record<string, unknown>).summary) }
|
|
66
|
+
: null;
|
|
67
|
+
const commits: Commit[] = (Array.isArray(body.commits) ? body.commits : []).flatMap((c) => {
|
|
68
|
+
const r = c as Record<string, unknown> | null;
|
|
69
|
+
if (!r || typeof r.commit !== 'string' || !r.commit) return [];
|
|
70
|
+
return [{
|
|
71
|
+
commit: r.commit,
|
|
72
|
+
parents: Array.isArray(r.parents) ? r.parents.filter((p): p is string => typeof p === 'string') : [],
|
|
73
|
+
at: num(r.at), message: text(r.message), body: text(r.body),
|
|
74
|
+
tags: Array.isArray(r.tags) ? r.tags.filter((t): t is string => typeof t === 'string' && !!t) : [],
|
|
75
|
+
summary: summaryOf(r.summary),
|
|
76
|
+
}];
|
|
77
|
+
});
|
|
78
|
+
const f = s.files as Record<string, unknown> | null | undefined;
|
|
79
|
+
const files = f && typeof f === 'object' ? { held: num(f.held), heldBytes: num(f.heldBytes), ignored: num(f.ignored) } : null;
|
|
80
|
+
return { status: { branch: s.branch, head: typeof s.head === 'string' ? s.head : null, branches, changes, files }, commits };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* What changed, as the parts of a sentence, in the order a person reads them: scenes before sounds, added before
|
|
85
|
+
* edited before removed. Each is a string key and its count (the sentence is the strings' job). Nothing that a scene
|
|
86
|
+
* or a sound tells: 'look' (the picture's size, or a file the scenes share) when that changed, else 'same'.
|
|
87
|
+
*/
|
|
88
|
+
export function summaryParts(summary: Summary): Array<{ key: string; n: number }> {
|
|
89
|
+
const parts: Array<{ key: string; n: number }> = [];
|
|
90
|
+
for (const [what, c] of [['scenes', summary.scenes], ['sounds', summary.sounds]] as const) {
|
|
91
|
+
if (c.added) parts.push({ key: `${what}Added`, n: c.added });
|
|
92
|
+
if (c.edited) parts.push({ key: `${what}Edited`, n: c.edited });
|
|
93
|
+
if (c.removed) parts.push({ key: `${what}Removed`, n: c.removed });
|
|
94
|
+
}
|
|
95
|
+
if (!parts.length) parts.push({ key: summary.stage || summary.other ? 'look' : 'same', n: 0 });
|
|
96
|
+
return parts;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type HistoryAge =
|
|
100
|
+
| { unit: 'now' }
|
|
101
|
+
| { unit: 'minute' | 'hour' | 'day'; n: number };
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* How long ago, as a unit and a number (the sentence is the strings' job). An unreadable time gives null: no time is
|
|
105
|
+
* better than "NaN minutes ago". A clock slightly ahead gives a future time; that counts as just now.
|
|
106
|
+
*/
|
|
107
|
+
export function historyAge(at: number, now: number): HistoryAge | null {
|
|
108
|
+
if (!Number.isFinite(at) || at <= 0) return null;
|
|
109
|
+
const minutes = Math.floor((now - at) / 60_000);
|
|
110
|
+
if (minutes < 1) return { unit: 'now' };
|
|
111
|
+
if (minutes < 60) return { unit: 'minute', n: minutes };
|
|
112
|
+
const hours = Math.floor(minutes / 60);
|
|
113
|
+
if (hours < 24) return { unit: 'hour', n: hours };
|
|
114
|
+
return { unit: 'day', n: Math.floor(hours / 24) };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A history call's outcome: done (with what came back), or refused with the server's `code`. */
|
|
118
|
+
export type HistoryResult<T = Record<string, unknown>> = { ok: true; body: T } | { ok: false; status: number; code: string | null; commits?: number };
|
|
119
|
+
|
|
120
|
+
async function call<T = Record<string, unknown>>(projectId: string, path: string, method = 'GET', json?: unknown): Promise<HistoryResult<T>> {
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(`/api/projects/${encodeURIComponent(projectId)}/history${path}`, {
|
|
123
|
+
method,
|
|
124
|
+
...(json === undefined ? {} : { headers: { 'content-type': 'application/json' }, body: JSON.stringify(json) }),
|
|
125
|
+
});
|
|
126
|
+
const body = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
127
|
+
if (res.ok) return { ok: true, body: body as T };
|
|
128
|
+
return { ok: false, status: res.status, code: typeof body.code === 'string' ? body.code : null, ...(typeof body.commits === 'number' ? { commits: body.commits } : {}) };
|
|
129
|
+
} catch {
|
|
130
|
+
return { ok: false, status: 503, code: null };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const fileList = (raw: unknown): HistoryFile[] => (Array.isArray(raw) ? raw : []).flatMap((f) => {
|
|
135
|
+
const r = f as Record<string, unknown> | null;
|
|
136
|
+
return r && typeof r.path === 'string' ? [{ path: r.path, size: num(r.size) }] : [];
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
export const historyApi = {
|
|
140
|
+
load: async (id: string) => {
|
|
141
|
+
const r = await call(id, '');
|
|
142
|
+
return r.ok ? normalizeHistory(r.body) : null;
|
|
143
|
+
},
|
|
144
|
+
files: async (id: string): Promise<{ held: HistoryFile[]; ignored: HistoryFile[] } | null> => {
|
|
145
|
+
const r = await call(id, '/files');
|
|
146
|
+
return r.ok ? { held: fileList(r.body.held), ignored: fileList(r.body.ignored) } : null;
|
|
147
|
+
},
|
|
148
|
+
commit: (id: string, message: string) => call(id, '/commit', 'POST', { message }),
|
|
149
|
+
discard: (id: string) => call(id, '/discard', 'POST', {}),
|
|
150
|
+
restore: (id: string, commit: string) => call(id, '/restore', 'POST', { commit }),
|
|
151
|
+
checkout: (id: string, o: { branch: string; create?: boolean; from?: string; carry?: 'leave' | 'bring' }) =>
|
|
152
|
+
call<{ parkedBack?: boolean }>(id, '/checkout', 'POST', o),
|
|
153
|
+
rename: (id: string, from: string, name: string) => call(id, `/branches/${encodeURIComponent(from)}`, 'PATCH', { name }),
|
|
154
|
+
remove: (id: string, name: string, force = false) => call(id, `/branches/${encodeURIComponent(name)}${force ? '?force=1' : ''}`, 'DELETE'),
|
|
155
|
+
merge: (id: string, o: { branch: string; message: string; prefer?: 'ours' | 'theirs' }) => call<{ merged?: boolean }>(id, '/merge', 'POST', o),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** The picture of a commit (taken when it was committed). */
|
|
159
|
+
export const commitPoster = (projectId: string, commit: string) =>
|
|
160
|
+
`/api/projects/${encodeURIComponent(projectId)}/history/poster/${commit}`;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Which sentence a refusal gets: always our own words, never the server's text. Codes the panel handles with a
|
|
164
|
+
* question of its own ('dirty' before going back, 'conflict' on a merge, 'unmerged' on a delete) are asked, not said.
|
|
165
|
+
*/
|
|
166
|
+
export function historyErrorKey(result: { status: number; code: string | null }): string {
|
|
167
|
+
switch (result.code) {
|
|
168
|
+
case 'dirty': return 'versions.errorDirty';
|
|
169
|
+
case 'nothing': return 'versions.errorNothing';
|
|
170
|
+
case 'conflict': return 'versions.errorCarry';
|
|
171
|
+
case 'exists': return 'versions.errorExists';
|
|
172
|
+
case 'name': return 'versions.errorName';
|
|
173
|
+
case 'message': return 'versions.errorMessage';
|
|
174
|
+
case 'current': return 'versions.errorCurrent';
|
|
175
|
+
default: break;
|
|
176
|
+
}
|
|
177
|
+
if (result.status === 404 || result.status === 501 || result.status === 503) return 'versions.errorUnavailable';
|
|
178
|
+
return 'versions.errorGeneric';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A progress event's numbers, checked: null when it is not one. */
|
|
182
|
+
export function historyProgressOf(raw: unknown): HistoryProgress | null {
|
|
183
|
+
const r = raw as Record<string, unknown> | null;
|
|
184
|
+
if (!r || r.type !== 'history-progress') return null;
|
|
185
|
+
const phase = r.phase === 'scan' || r.phase === 'store' || r.phase === 'write' || r.phase === 'migrate' ? r.phase : null;
|
|
186
|
+
if (!phase) return null;
|
|
187
|
+
return { op: text(r.op), phase, done: num(r.done), total: num(r.total) };
|
|
188
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starting from an existing film, on the Projects page: a folder opened (the web editor's own folder browser, or the
|
|
3
|
+
* app's native dialog), a film got from GitHub, an example, and a moved project located again. The calls and what
|
|
4
|
+
* the dialogs work out from their answers (studio/server/browse.mjs, fetch-repo.mjs, examples.mjs).
|
|
5
|
+
*
|
|
6
|
+
* GET /api/browse?path= → a folder's folders (only under the home folder), and places to start
|
|
7
|
+
* POST /api/projects { path, create: false } → { project }, or 409 code 'no-film' (ask before starting one there)
|
|
8
|
+
* POST /api/projects/fetch { url } → { job }; GET / DELETE /api/projects/fetch/:id
|
|
9
|
+
* GET /api/examples → { examples } (none when the index cannot be read)
|
|
10
|
+
* GET /api/skills?project=<id> → { skills } (the project's, the person's own, the official ones)
|
|
11
|
+
* POST /api/projects/:id/locate { path } → { project }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type BrowseFolder = { name: string; path: string; film: boolean };
|
|
15
|
+
export type BrowseListing = {
|
|
16
|
+
path: string;
|
|
17
|
+
root: string;
|
|
18
|
+
parent: string | null;
|
|
19
|
+
name: string;
|
|
20
|
+
film: boolean;
|
|
21
|
+
folders: BrowseFolder[];
|
|
22
|
+
more: boolean;
|
|
23
|
+
places: { id: string; path: string }[];
|
|
24
|
+
recent: string[];
|
|
25
|
+
};
|
|
26
|
+
export type FetchPhase = 'lookup' | 'download' | 'checkout' | 'copy';
|
|
27
|
+
export type FetchJob = {
|
|
28
|
+
id: string;
|
|
29
|
+
url: string;
|
|
30
|
+
label: string;
|
|
31
|
+
state: 'running' | 'done' | 'failed';
|
|
32
|
+
phase: FetchPhase;
|
|
33
|
+
percent: number | null;
|
|
34
|
+
bytes: number;
|
|
35
|
+
/** the size of the whole, when it is known before it downloads (GitHub over https); null through git */
|
|
36
|
+
total: number | null;
|
|
37
|
+
error?: string;
|
|
38
|
+
code?: string;
|
|
39
|
+
project?: { id: string };
|
|
40
|
+
};
|
|
41
|
+
/** a skill a project can be made with (openfilm/src/skills.mjs): the project's own, the person's, or official */
|
|
42
|
+
export type SkillEntry = {
|
|
43
|
+
name: string; description: string; source: 'project' | 'mine' | 'official'; scripts: number;
|
|
44
|
+
/** how people see it (its openfilm.json): a title and a one-line summary, by language */
|
|
45
|
+
shown?: { title: Record<string, string>; summary: Record<string, string>; details?: Record<string, string> };
|
|
46
|
+
/** its openfilm.json's (else its front matter's) */
|
|
47
|
+
version?: string;
|
|
48
|
+
/** a way of working (a kind of job) or a look; its place among its kind; its mark */
|
|
49
|
+
kind?: 'workflow' | 'style'; order?: number; icon?: string;
|
|
50
|
+
/** addresses of its cover picture and preview video, when it has them */
|
|
51
|
+
cover?: string; preview?: string;
|
|
52
|
+
/** where its source can be read (a repository's address) */
|
|
53
|
+
repository?: string;
|
|
54
|
+
/** the person's own: where it came from (a link, a folder), or made where it is */
|
|
55
|
+
origin?: { from: 'link' | 'folder'; address: string } | { from: 'made' };
|
|
56
|
+
/** an official one: installed to be used (the person's own and a project's are installed already) */
|
|
57
|
+
installed?: boolean;
|
|
58
|
+
/** turned off: offered to no agent until it is turned on again */
|
|
59
|
+
off?: true;
|
|
60
|
+
};
|
|
61
|
+
/** an example film's part, as its index names it (`S2-Hook`), from and to in seconds */
|
|
62
|
+
export type ExampleSegment = { title: string; from: number; to: number };
|
|
63
|
+
export type Example = {
|
|
64
|
+
id: string; title: string; description: string; poster: string | null; video: string | null; duration: number | null; url: string;
|
|
65
|
+
category: string | null; segments: ExampleSegment[];
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export class SourceError extends Error {
|
|
69
|
+
readonly status: number;
|
|
70
|
+
readonly code: string | null;
|
|
71
|
+
constructor(message: string, status: number, code: string | null) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.status = status;
|
|
74
|
+
this.code = code;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function call<T>(path: string, method = 'GET', json?: unknown): Promise<T> {
|
|
79
|
+
const res = await fetch(`/api/${path}`, {
|
|
80
|
+
method,
|
|
81
|
+
...(json === undefined ? {} : { headers: { 'content-type': 'application/json' }, body: JSON.stringify(json) }),
|
|
82
|
+
});
|
|
83
|
+
const body = await res.json().catch(() => ({}));
|
|
84
|
+
if (!res.ok) throw new SourceError(String(body.error ?? `Studio answered ${res.status}`), res.status, typeof body.code === 'string' ? body.code : null);
|
|
85
|
+
return body as T;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* what the galleries read last: shown at once when one opens, and asked for again behind it */
|
|
89
|
+
const seen: { examples: Example[] | null; skills: Map<string, SkillEntry[]> } = { examples: null, skills: new Map() };
|
|
90
|
+
/** The examples read last, or null. */
|
|
91
|
+
export const seenExamples = (): Example[] | null => seen.examples;
|
|
92
|
+
/** The skills read last for a project (else for no project: the official ones and the person's own), or null. */
|
|
93
|
+
export const seenSkills = (projectId?: string | null): SkillEntry[] | null => seen.skills.get(projectId ?? '') ?? seen.skills.get('') ?? null;
|
|
94
|
+
|
|
95
|
+
/** The desktop's add dialog and Studio's gallery share the window. */
|
|
96
|
+
export const SKILLS_CHANGED = 'openfilm:skills-changed';
|
|
97
|
+
|
|
98
|
+
/** Read an open gallery's skills again after an addition; a stale request must not replace the newer list. */
|
|
99
|
+
export function watchSkills(projectId: string | null | undefined, changed: (skills: SkillEntry[]) => void, events: EventTarget = window): () => void {
|
|
100
|
+
let revision = 0;
|
|
101
|
+
const refresh = () => {
|
|
102
|
+
const request = ++revision;
|
|
103
|
+
void sourcesApi.skills(projectId).then((skills) => { if (request === revision) changed(skills); });
|
|
104
|
+
};
|
|
105
|
+
events.addEventListener(SKILLS_CHANGED, refresh);
|
|
106
|
+
refresh();
|
|
107
|
+
return () => { ++revision; events.removeEventListener(SKILLS_CHANGED, refresh); };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const sourcesApi = {
|
|
111
|
+
browse: (path?: string | null) => call<BrowseListing>(`browse${path ? `?path=${encodeURIComponent(path)}` : ''}`),
|
|
112
|
+
/** opens the folder only when it holds a film: else a SourceError with code 'no-film' (or 'not-found') */
|
|
113
|
+
openExisting: (path: string) => call<{ project: { id: string } }>('projects', 'POST', { path, create: false }).then((r) => r.project),
|
|
114
|
+
startFetch: (url: string) => call<{ job: FetchJob }>('projects/fetch', 'POST', { url }).then((r) => r.job),
|
|
115
|
+
fetchJob: (id: string) => call<{ job: FetchJob }>(`projects/fetch/${encodeURIComponent(id)}`).then((r) => r.job),
|
|
116
|
+
cancelFetch: (id: string) => call<object>(`projects/fetch/${encodeURIComponent(id)}`, 'DELETE'),
|
|
117
|
+
examples: () => call<{ examples: Example[] }>('examples').then((r) => (seen.examples = r.examples), () => seen.examples ?? []),
|
|
118
|
+
/** an example's folder on this computer, fetched when needed (for an agent to read how it is made) */
|
|
119
|
+
exampleFolder: (id: string) => call<{ folder: string }>(`examples/${encodeURIComponent(id)}/folder`, 'POST').then((r) => r.folder),
|
|
120
|
+
skills: (projectId?: string | null) => call<{ skills: SkillEntry[] }>(`skills${projectId ? `?project=${encodeURIComponent(projectId)}` : ''}`)
|
|
121
|
+
.then((r) => { seen.skills.set(projectId ?? '', r.skills); return r.skills; }, () => seenSkills(projectId) ?? []),
|
|
122
|
+
setSkillCover: async (name: string, cover: string | null, projectId?: string | null): Promise<string | null> => {
|
|
123
|
+
const path = `skills/${encodeURIComponent(name)}/cover${projectId ? `?project=${encodeURIComponent(projectId)}` : ''}`;
|
|
124
|
+
const result = await call<{ cover: string | null }>(path, cover === null ? 'DELETE' : 'PUT', cover === null ? undefined : { cover });
|
|
125
|
+
for (const [key, skills] of seen.skills) seen.skills.set(key, skills.map((skill) => skill.source === 'mine' && skill.name === name ? { ...skill, cover: result.cover ?? undefined } : skill));
|
|
126
|
+
return result.cover;
|
|
127
|
+
},
|
|
128
|
+
/** a skill turned on or off for every project; whoever lists skills (the gallery, the desktop's chat) reads them again */
|
|
129
|
+
setSkillOn: async (name: string, on: boolean): Promise<void> => {
|
|
130
|
+
await call<{ on: boolean }>(`skills/${encodeURIComponent(name)}/on`, 'PUT', { on });
|
|
131
|
+
for (const [key, skills] of seen.skills) seen.skills.set(key, skills.map((skill) => {
|
|
132
|
+
if (skill.name !== name) return skill;
|
|
133
|
+
const { off: _was, ...rest } = skill;
|
|
134
|
+
return on ? rest : { ...rest, off: true as const };
|
|
135
|
+
}));
|
|
136
|
+
window.dispatchEvent(new Event(SKILLS_CHANGED));
|
|
137
|
+
},
|
|
138
|
+
/** an official skill installed (it starts on) or uninstalled; whoever lists skills reads them again */
|
|
139
|
+
setSkillInstalled: async (name: string, installed: boolean): Promise<void> => {
|
|
140
|
+
await call<{ installed: boolean }>(`skills/${encodeURIComponent(name)}/installed`, 'PUT', { installed });
|
|
141
|
+
window.dispatchEvent(new Event(SKILLS_CHANGED));
|
|
142
|
+
},
|
|
143
|
+
/** one of the person's own skills, or a project's, moved to the trash */
|
|
144
|
+
removeSkill: async (name: string, projectId?: string | null): Promise<void> => {
|
|
145
|
+
await call<object>(`skills/${encodeURIComponent(name)}${projectId ? `?project=${encodeURIComponent(projectId)}` : ''}`, 'DELETE');
|
|
146
|
+
window.dispatchEvent(new Event(SKILLS_CHANGED));
|
|
147
|
+
},
|
|
148
|
+
/** asked as Studio opens, so a gallery opened later shows its examples and skills at once */
|
|
149
|
+
preload: () => { void sourcesApi.examples(); void sourcesApi.skills(null); },
|
|
150
|
+
locate: (id: string, path: string) => call<{ project: { id: string } }>(`projects/${encodeURIComponent(id)}/locate`, 'POST', { path }).then((r) => r.project),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Whether what was pasted looks like a repository's address (the server checks it for real). */
|
|
154
|
+
export function looksLikeRepoUrl(text: string): boolean {
|
|
155
|
+
return /^(https:\/\/)?(www\.)?(github|gitlab)\.com\/[^/\s]+\/[^/\s]+/i.test(text.trim());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A size as people read it: "820 KB", "12.4 MB", "1.2 GB". */
|
|
159
|
+
export function formatBytes(bytes: number): string {
|
|
160
|
+
if (!Number.isFinite(bytes) || bytes < 1024 * 1024) return `${Math.max(0, Math.round((bytes || 0) / 1024))} KB`;
|
|
161
|
+
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
162
|
+
return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* How fast a download goes, in bytes a second: what came over the last few seconds (`windowMs`), so a stall shows
|
|
167
|
+
* soon and a burst does not jump it. Null until it has a second to go on.
|
|
168
|
+
*/
|
|
169
|
+
export function createSpeedMeter(windowMs = 5000): (bytes: number, at: number) => number | null {
|
|
170
|
+
const samples: { at: number; bytes: number }[] = [];
|
|
171
|
+
return (bytes, at) => {
|
|
172
|
+
/* fewer bytes than before: a new download, measured afresh */
|
|
173
|
+
if (samples.length && bytes < samples[samples.length - 1].bytes) samples.length = 0;
|
|
174
|
+
samples.push({ at, bytes });
|
|
175
|
+
while (samples.length > 2 && at - samples[1].at >= windowMs) samples.shift();
|
|
176
|
+
const span = (at - samples[0].at) / 1000;
|
|
177
|
+
return span >= 1 ? Math.max(0, (bytes - samples[0].bytes) / span) : null;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Seconds a download has left at `speed`; null when that cannot be told (no total, or nothing coming). */
|
|
182
|
+
export function secondsLeft(bytes: number, total: number | null, speed: number | null): number | null {
|
|
183
|
+
if (!total || !speed || speed <= 0) return null;
|
|
184
|
+
return Math.max(0, (total - bytes) / speed);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** A length in seconds as m:ss. */
|
|
188
|
+
export function formatSeconds(seconds: number): string {
|
|
189
|
+
const total = Math.max(0, Math.round(seconds));
|
|
190
|
+
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The breadcrumbs from `root` (the home folder, shown by `rootName`) to `path`: each a name and the folder it opens.
|
|
195
|
+
* Paths with / or \ alike; a path not under `root` is one crumb.
|
|
196
|
+
*/
|
|
197
|
+
export function pathCrumbs(path: string, root: string, rootName = '~'): { name: string; path: string }[] {
|
|
198
|
+
const sep = path.includes('\\') && !path.includes('/') ? '\\' : '/';
|
|
199
|
+
const trim = (p: string) => (p.length > 1 && (p.endsWith('/') || p.endsWith('\\')) ? p.slice(0, -1) : p);
|
|
200
|
+
const at = trim(path);
|
|
201
|
+
const top = trim(root);
|
|
202
|
+
const inside = at === top || at.startsWith(top === sep ? top : `${top}${sep}`);
|
|
203
|
+
if (!inside) return [{ name: at, path: at }];
|
|
204
|
+
const crumbs = [{ name: rootName, path: top }];
|
|
205
|
+
let walked = top;
|
|
206
|
+
for (const part of at.slice(top.length).split(sep).filter(Boolean)) {
|
|
207
|
+
walked = `${walked}${sep}${part}`;
|
|
208
|
+
crumbs.push({ name: part, path: walked });
|
|
209
|
+
}
|
|
210
|
+
return crumbs;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The words for a failed fetch, by its code (i18n keys under projects.fetch); null: say the server's own message. */
|
|
214
|
+
export function fetchErrorKey(code: string | null | undefined): string | null {
|
|
215
|
+
switch (code) {
|
|
216
|
+
case 'url':
|
|
217
|
+
case 'host':
|
|
218
|
+
case 'access':
|
|
219
|
+
case 'network':
|
|
220
|
+
case 'ref':
|
|
221
|
+
case 'no-folder':
|
|
222
|
+
case 'no-film':
|
|
223
|
+
case 'too-big':
|
|
224
|
+
case 'timeout':
|
|
225
|
+
case 'git':
|
|
226
|
+
return `projects.fetch.error.${code === 'no-folder' ? 'noFolder' : code === 'no-film' ? 'noFilm' : code === 'too-big' ? 'tooBig' : code}`;
|
|
227
|
+
default:
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The words for where a fetch is (i18n key under projects.fetch). */
|
|
233
|
+
export function fetchPhaseKey(phase: FetchPhase): string {
|
|
234
|
+
return `projects.fetch.phase.${phase}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Wait on a fetch job until it is done or failed, telling each state on the way. `stop` ends the waiting (the job runs
|
|
239
|
+
* on unless cancelled). `get` and `wait` are the API's and a timer, so a test can drive it.
|
|
240
|
+
*/
|
|
241
|
+
export function followFetch(
|
|
242
|
+
first: FetchJob,
|
|
243
|
+
onJob: (job: FetchJob) => void,
|
|
244
|
+
{ get = sourcesApi.fetchJob, every = 400, wait = (ms: number) => new Promise<void>((done) => setTimeout(done, ms)) }: {
|
|
245
|
+
get?: (id: string) => Promise<FetchJob>;
|
|
246
|
+
every?: number;
|
|
247
|
+
wait?: (ms: number) => Promise<void>;
|
|
248
|
+
} = {},
|
|
249
|
+
): { stop: () => void; done: Promise<FetchJob | null> } {
|
|
250
|
+
let stopped = false;
|
|
251
|
+
const done = (async () => {
|
|
252
|
+
let job = first;
|
|
253
|
+
onJob(job);
|
|
254
|
+
let misses = 0;
|
|
255
|
+
while (!stopped && job.state === 'running') {
|
|
256
|
+
await wait(every);
|
|
257
|
+
if (stopped) break;
|
|
258
|
+
try {
|
|
259
|
+
job = await get(job.id);
|
|
260
|
+
misses = 0;
|
|
261
|
+
} catch (e) {
|
|
262
|
+
/* Studio restarting: a few misses are waited out; a job it no longer knows has ended */
|
|
263
|
+
if (++misses < 10 && !(e instanceof SourceError && e.status === 404)) continue;
|
|
264
|
+
job = { ...job, state: 'failed', error: e instanceof Error ? e.message : String(e), code: 'lost' };
|
|
265
|
+
}
|
|
266
|
+
if (!stopped) onJob(job);
|
|
267
|
+
}
|
|
268
|
+
return stopped ? null : job;
|
|
269
|
+
})();
|
|
270
|
+
return { stop: () => { stopped = true; }, done };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const COLLAPSE_KEY = 'openfilm.projects.examplesCollapsed';
|
|
274
|
+
/** Whether the person folded the examples away (kept in this browser). */
|
|
275
|
+
export function examplesCollapsed(storage: Pick<Storage, 'getItem'> | null): boolean {
|
|
276
|
+
try { return storage?.getItem(COLLAPSE_KEY) === '1'; } catch { return false; }
|
|
277
|
+
}
|
|
278
|
+
export function setExamplesCollapsed(storage: Pick<Storage, 'setItem' | 'removeItem'> | null, collapsed: boolean): void {
|
|
279
|
+
try {
|
|
280
|
+
if (collapsed) storage?.setItem(COLLAPSE_KEY, '1');
|
|
281
|
+
else storage?.removeItem(COLLAPSE_KEY);
|
|
282
|
+
} catch { /* not kept */ }
|
|
283
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small rules of the Projects page: how long its list is waited for, and when a card asks again for a poster that is
|
|
3
|
+
* still being drawn.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** How long the list is waited for before the page says it could not load it (and offers to try again). */
|
|
7
|
+
export const LIST_WAIT_MS = 15_000;
|
|
8
|
+
|
|
9
|
+
/** What `promise` gives, or a failure once `ms` have passed without it. */
|
|
10
|
+
export function within<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
11
|
+
return new Promise<T>((done, fail) => {
|
|
12
|
+
const timer = setTimeout(() => fail(new Error(`no answer within ${ms / 1000} s`)), ms);
|
|
13
|
+
promise.then(
|
|
14
|
+
(value) => { clearTimeout(timer); done(value); },
|
|
15
|
+
(error: unknown) => { clearTimeout(timer); fail(error); },
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A card's poster that did not come (still being drawn, which Studio answers at once rather than hold the browser's
|
|
22
|
+
* connection): asked for again after this many ms, `tried` times so far; null: given up (the card keeps its face).
|
|
23
|
+
*/
|
|
24
|
+
export function posterRetryDelay(tried: number): number | null {
|
|
25
|
+
return tried < 4 ? 4000 * 2 ** tried : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The poster's address for try `n` (0: the first), so the browser asks Studio again rather than its cache. */
|
|
29
|
+
export function posterTry(url: string, n: number): string {
|
|
30
|
+
return n ? `${url}${url.includes('?') ? '&' : '?'}try=${n}` : url;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether a click on a card opens it: not the second click of a double-click (`detail` 2 or more). The first click
|
|
35
|
+
* opened something already, a folder say, and what stands under the pointer now was not aimed at. A key press (Enter,
|
|
36
|
+
* Space: `detail` 0) opens.
|
|
37
|
+
*/
|
|
38
|
+
export function opensOnClick(event: { detail: number }): boolean {
|
|
39
|
+
return event.detail < 2;
|
|
40
|
+
}
|