@linker-design-plus/timeline-track 2.1.0 → 2.1.1

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/README.md CHANGED
@@ -2,24 +2,7 @@
2
2
 
3
3
  基于 TypeScript、Konva 和 Vue demo 的时间线编辑组件库,对外以 `TimelineManager` 为统一入口,提供多轨片段编辑、拖拽/拉伸/分割、撤销重做、选中态同步、封面渲染和预览挂载能力。时间轴主体仍由 Konva 驱动,播放指针使用独立的 SVG/DOM 覆盖层渲染。
4
4
 
5
- ## 当前状态
6
-
7
- - 主线重构已经完成,仓库已从“双轨实现 + 大量兼容层”收敛到单一轨道组件 `Track`
8
- - `TimelineManager` 仍然是 façade,但内部已经拆分为 `stores`、`commands`、`controllers`、`tracks`、`presentation`
9
- - 当前发布包实际内置的预览实现是 `DomPreviewBackend`
10
- - `previewBackend` 配置仍然保留,但当前实现会统一解析到 `dom`,不要把 `canvas` / `auto` 视为已交付能力
11
- - 仓库内保留了较完整的回归测试,适合继续做功能收敛和架构瘦身
12
-
13
- ## 审计结论
14
-
15
- 本轮已完成一次针对仓库现状的维护梳理,并同步修正了以下残留:
16
-
17
- - 修复了 3 处测试残留,使 `pnpm test` 与 `pnpm exec tsc -p tsconfig.json --noEmit` 恢复通过
18
- - 清理了 `pixi` 预览类型和 demo 入口中的过期选项
19
- - 把 README 与重构路线文档改为“当前真实状态”,不再把未交付的 canvas 预览写成已完成能力
20
- - 把 `build` 脚本改为直接调用本地 `tsc`,避免 `npm run build:types` 带来的环境告警
21
-
22
- 更细的维护记录见 [docs/maintenance-audit.md](./docs/maintenance-audit.md)。
5
+ 当前版本已经收敛到单一 `Track` 实现。预览侧真实交付的 backend 只有 `DomPreviewBackend`;`previewBackend` 配置仍保留兼容输入,但运行时会统一解析到 `dom`。
23
6
 
24
7
  ## 安装
25
8
 
@@ -66,35 +49,17 @@ timeline.on('history_change', (_event, data) => {
66
49
  });
67
50
  ```
68
51
 
69
- ### 预览资源缓存
70
-
71
- 预览资源缓存默认关闭。开启后,可被前端 `fetch` 读取且不超过 100MB 的 `http:` / `https:` 音视频文件会被缓存到浏览器存储中,用于后续预览播放。默认策略为 30 天 TTL、10GB 应用级容量上限、优先 OPFS,并在 OPFS 不可用时降级到 IndexedDB Blob。缓存失败会自动回退原始媒体 URL。
72
-
73
- ```ts
74
- const timeline = new TimelineManager({
75
- container,
76
- previewBackend: 'dom',
77
- resourceCache: {
78
- enabled: true,
79
- resolveMode: 'prefer-fast-start'
80
- }
81
- });
82
- ```
83
-
84
- 第一版不会缓存 HLS manifest / 分片,也不会缓存超过 100MB 的媒体文件。
85
-
86
52
  ## 核心能力
87
53
 
88
- - 多轨时间线编辑,支持视频轨和音频轨
54
+ - 多轨时间线编辑,支持视频轨、音频轨和文本片段
89
55
  - 片段拖拽、拉伸、分割、跨轨移动和重叠避让
90
- - 单一选中态与批量拖拽
56
+ - 多选、框选与批量拖拽
91
57
  - 播放头、缩放、滚动和时间同步
92
58
  - 撤销 / 重做 / 历史变更通知
93
- - 缩略图提供器与异步封面加载
94
- - 预览容器挂载、预览比例同步和当前激活片段解析
95
- - 草稿导入导出、音视频分离/恢复等编辑工作流
59
+ - 预览挂载、比例同步和当前激活片段解析
60
+ - 草稿导入导出、音视频分离 / 恢复等编辑工作流
96
61
 
97
- ## 主要 API
62
+ ## API 概览
98
63
 
99
64
  常用方法:
100
65
 
@@ -104,30 +69,30 @@ const timeline = new TimelineManager({
104
69
  - `setSpeed(speed)` / `getSpeed()`
105
70
  - `addClip(config)` / `addClips(configs)`
106
71
  - `updateClip(clipId, updates)` / `removeClip(clipId)`
107
- - `selectClip(clipId)` / `clearSelection()` / `getSelectedClip()`
72
+ - `selectClip(clipId)` / `setSelection(ids)` / `clearSelection()`
108
73
  - `splitCurrentClip()` / `removeClipGaps()`
109
- - `getRenderedHeight()`
110
74
  - `undo()` / `redo()` / `clearHistory()`
111
75
  - `attachPreview(containerOrConfig)` / `detachPreview()`
112
- - `exportTimeline()` / `loadDraft(data)`
76
+ - `exportTimeline()` / `importTimeline(data)`
113
77
 
114
- 快捷键配置:
78
+ 配音配置面板:
115
79
 
116
- - `keyboardShortcuts: false` 可彻底关闭快捷键
117
- - `keyboardShortcuts.bindings` 可按动作覆盖默认键位
118
- - 时间轴挂载后默认在当前页面生效
80
+ - `attachClipConfig(container, options)` 返回 `TimelineClipConfigController`
81
+ - 内建 `generateVoiceBatch` 流程会自动展示 / 关闭“配音生成中”全屏 loading
82
+ - 外部自定义配音流程也可以手动调用 `controller.showVoiceGenerationLoading()` 和 `controller.hideVoiceGenerationLoading()`
119
83
 
120
- 默认快捷键:
84
+ ```ts
85
+ const controller = timeline.attachClipConfig(clipConfigContainer, {
86
+ voiceCatalog
87
+ });
121
88
 
122
- - 播放 / 暂停:`Space`
123
- - 删除片段:`Delete` / `Backspace`
124
- - 复制片段:`Mod+C`
125
- - 剪切片段:`Mod+X`
126
- - 粘贴片段:`Mod+V`
127
- - 分离 / 还原音频:`Mod+Alt+A`
128
- - 分割片段:`Mod+B`
129
- - 撤销:`Mod+Z`
130
- - 还原:`Mod+Shift+Z`,Windows 另支持 `Ctrl+Y`
89
+ controller.showVoiceGenerationLoading();
90
+ try {
91
+ await customGenerateVoice();
92
+ } finally {
93
+ controller.hideVoiceGenerationLoading();
94
+ }
95
+ ```
131
96
 
132
97
  常用事件:
133
98
 
@@ -137,86 +102,30 @@ const timeline = new TimelineManager({
137
102
  - `clip_added`
138
103
  - `clip_removed`
139
104
  - `clip_updated`
140
- - `clip_selected`
141
105
  - `selected_clip_change`
142
106
  - `history_change`
143
107
  - `track_duration_change`
144
108
  - `buffering_state_change`
145
109
  - `source_loading_change`
146
110
 
147
- ## 关键类型
148
-
149
- ```ts
150
- interface ClipConfig {
151
- id?: string;
152
- type?: 'video' | 'audio';
153
- externalId?: string;
154
- src: string;
155
- name: string;
156
- isMuted?: boolean;
157
- startTime?: number;
158
- duration: number;
159
- startTimeAtSource?: number;
160
- endTimeAtSource?: number;
161
- sourceDuration?: number;
162
- thumbnails?: string[];
163
- style?: Record<string, any>;
164
- visualTransform?: {
165
- x: number;
166
- y: number;
167
- scale: number;
168
- };
169
- trackId?: string;
170
- volume?: number;
171
- }
172
- ```
173
-
174
- ## 预览实现说明
111
+ ## 兼容与边界
175
112
 
176
- - 当前代码路径实际只会创建 `DomPreviewBackend`
177
- - `previewBackend` 字段被保留,目的是不给后续预览实现扩展制造额外 breaking change
178
- - 如果你在业务接入层看到 `canvas` / `auto` 配置,请把它当作保留参数,而不是当前版本的有效能力
179
- - 挂载预览且当前时间命中视频片段时,播放时间由预览视频的真实媒体时钟驱动;媒体 buffering 或 stalled 导致画面没有前进时,播放指针也不会继续超前
180
- - 多视频叠加播放时,任意一路 active 视频仍在加载或卡顿,预览层会暂停整个 active 播放组,等当前时刻所有视频都 ready 后统一恢复
113
+ - 当前代码路径只会创建 `DomPreviewBackend`
114
+ - `canvas` / `auto` 仍可作为兼容输入传入,但不应视为已交付能力
115
+ - 挂载预览且当前时间命中视频片段时,时间轴会消费预览视频的真实媒体时钟
181
116
  - 空白段、纯音频、文本或未挂载预览时,播放仍回退到 `TimelineManager` 的 wall-clock 推进
182
117
 
183
- ## 架构概览
184
-
185
- ```mermaid
186
- flowchart TB
187
- API["Public API\nsrc/index.ts"] --> FACADE["TimelineManager\nsrc/core/facade/timelineManager.ts"]
188
- FACADE --> STORES["Stores\nTimeline / Selection / Playback / Viewport"]
189
- FACADE --> COMMANDS["Commands\nTimelineCommands"]
190
- FACADE --> CONTROLLERS["Controllers\nworkflow / selection / duration / playback / events"]
191
- FACADE --> TRACKS["Tracks\nTrackManager / Bridge / Collection"]
192
- FACADE --> PRESENTATION["Presentation\nTimelinePresentationAdapter"]
193
- FACADE --> PREVIEW["Preview\nDomPreviewBackend"]
194
- FACADE --> COMPONENTS["Presentation Views\nTimeline / Track / Clip / Panels / ManagedPlayhead"]
195
- ```
118
+ ## 预览资源缓存
119
+
120
+ 预览资源缓存默认关闭。开启后,前端可直接 `fetch` 的音视频资源会被缓存到浏览器存储中,用于后续预览复用;缓存失败时会自动回退原始媒体 URL。完整边界和配置说明见 [docs/resource-cache.md](./docs/resource-cache.md)。
196
121
 
197
- 当前最值得关注的热点:
198
-
199
- - `src/core/facade/timelineManager.ts` 仍然超过 4k 行,是后续最重要的瘦身目标
200
- - `src/components/track/Track.tsx` 内部还保留一层 `legacy` 交互镜像,用于兼容旧拖拽状态
201
- - `src/utils/rendering/KonvaUtils.ts` 仍偏大,适合继续拆分为更小的渲染工具模块
202
- - `demo/App.vue` 既承担演示又承担诊断面板职责,维护成本偏高
203
-
204
- ## 目录结构
205
-
206
- ```text
207
- src/
208
- components/ 时间轴视图、SVG/DOM 覆盖层与交互 helper
209
- core/ stores / commands / controllers / facade / models
210
- utils/ 渲染、时间和日志工具
211
- styles/ 样式入口
212
- index.ts 对外导出
213
- docs/
214
- interaction-model.md
215
- maintenance-audit.md
216
- refactor-roadmap.md
217
- demo/
218
- App.vue
219
- main.ts
122
+ ```ts
123
+ const timeline = new TimelineManager({
124
+ container,
125
+ resourceCache: {
126
+ enabled: true
127
+ }
128
+ });
220
129
  ```
221
130
 
222
131
  ## 开发
@@ -229,10 +138,11 @@ pnpm test
229
138
  pnpm exec tsc -p tsconfig.json --noEmit
230
139
  ```
231
140
 
232
- ## 相关文档
141
+ ## 文档
233
142
 
143
+ - [docs/README.md](./docs/README.md): 文档入口
144
+ - [docs/architecture-overview.md](./docs/architecture-overview.md): 当前架构、模块边界、运行链路
145
+ - [docs/implementation-notes.md](./docs/implementation-notes.md): selection、preview、resource cache、draft 的实现笔记
146
+ - [docs/resource-cache.md](./docs/resource-cache.md): 预览资源缓存的能力边界与配置
234
147
  - [docs/interaction-model.md](./docs/interaction-model.md): 指针交互分层和拖拽约束
235
148
  - [docs/preview-playback-recovery-flow.md](./docs/preview-playback-recovery-flow.md): 预览媒体时钟驱动播放、buffering 阻塞与恢复续播流程
236
- - [docs/refactor-roadmap.md](./docs/refactor-roadmap.md): 主线重构归档与后续收敛方向
237
- - [docs/maintenance-audit.md](./docs/maintenance-audit.md): 本轮仓库审计、验证结果和依赖建议
238
- - [docs/review-remediation-plan.md](./docs/review-remediation-plan.md): 基于审查结论的修复计划与阶段性落地建议
@@ -105,6 +105,8 @@ export declare class ClipConfigPanel {
105
105
  setClip(clip: Clip | null): void;
106
106
  setSelectionState(selectionState: ClipConfigPanelSelectionState): void;
107
107
  setPreferredTab(tab: 'voice' | null): void;
108
+ showVoiceGenerationLoading(): void;
109
+ hideVoiceGenerationLoading(): void;
108
110
  setVoiceGenerationBusy(isBusy: boolean): void;
109
111
  destroy(): void;
110
112
  private render;
@@ -13,4 +13,5 @@ export { TimelinePreviewStateController, type TimelinePendingPreviewState } from
13
13
  export { TimelineKeyboardShortcutsController, type TimelineKeyboardShortcutsControllerCallbacks } from './timelineKeyboardShortcutsController';
14
14
  export * from './timelineSelectionController';
15
15
  export * from './timelineTrackMutationController';
16
+ export { TimelineClipConfigController } from './timelineClipConfigController';
16
17
  export { TimelineTrackInfoPanelController } from './timelineTrackInfoPanelController';
@@ -18,6 +18,8 @@ export declare class TimelineClipConfigController {
18
18
  updateFromExternal(): void;
19
19
  destroy(): void;
20
20
  setPreferredTab(tab: 'voice' | null): void;
21
+ showVoiceGenerationLoading(): void;
22
+ hideVoiceGenerationLoading(): void;
21
23
  setVoiceGenerationBusy(isBusy: boolean): void;
22
24
  private buildSelectionState;
23
25
  private resolveUpdateTargets;
@@ -85,6 +85,7 @@ export declare class TimelinePreviewSession {
85
85
  private readonly deferredPreloadSlotKeys;
86
86
  private deferredPreloadFlushScheduled;
87
87
  private lastSyncedPlayState;
88
+ private pendingClockAlignmentTargetTime;
88
89
  constructor(callbacks?: TimelinePreviewSessionCallbacks, dependencies?: TimelinePreviewSessionDependencies);
89
90
  private requestPauseIfPlaying;
90
91
  private emitDiagnostic;
@@ -162,6 +163,12 @@ export declare class TimelinePreviewSession {
162
163
  private buildPreviewClockStateFromSlot;
163
164
  private resolveClockBlockedReason;
164
165
  private getPreviewClockSourceSlot;
166
+ private buildPreviewClockSourceCandidate;
167
+ private comparePreviewClockSourceCandidates;
168
+ private isPreviewClockCandidateAlignedToTarget;
169
+ private getPreviewClockTargetAlignmentThresholdMs;
170
+ private isPreviewClockSlotAlignedToPendingTarget;
171
+ private updatePendingClockAlignmentTarget;
165
172
  private syncActivePlaybackGroupSuspension;
166
173
  private getSlotMediaTimeMs;
167
174
  private getSlotTimelineTime;
@@ -61,6 +61,7 @@ export declare class TimelineManager {
61
61
  private lastSelectedClipId;
62
62
  private previewAspectRatio;
63
63
  private readonly bodyViewportScrollListener;
64
+ private stageMouseDownListener;
64
65
  private readonly bodyCanvasHostClickListener;
65
66
  private readonly rootWheelListener;
66
67
  private keyboardShortcutsController;
@@ -155,7 +156,6 @@ export declare class TimelineManager {
155
156
  private getTracksSortedByOrder;
156
157
  private getPlaybackTracksSnapshot;
157
158
  private buildPlaybackPlan;
158
- private hasTimelineVideoClip;
159
159
  private playbackPlanHasActiveVideoClip;
160
160
  private hasActiveTimelineVideoClip;
161
161
  private getPreviewAutoAspectRatioClipOrderMap;
@@ -169,6 +169,8 @@ export declare class TimelineManager {
169
169
  private syncPreviewPlaybackStateIfNeeded;
170
170
  private shouldUsePreviewClockPlayback;
171
171
  private handlePreviewClockStateChange;
172
+ private isPreviewClockForCurrentActiveClip;
173
+ private shouldCommitUnavailablePreviewClock;
172
174
  private commitPlaybackTimeFromPreviewClock;
173
175
  private syncPreviewAfterPreviewClockCommitIfNeeded;
174
176
  private resumeWallClockPlaybackIfPreviewClockUnavailable;
@@ -353,6 +355,9 @@ export declare class TimelineManager {
353
355
  private findClipById;
354
356
  private buildGeneratedAudioClipName;
355
357
  private normalizeGeneratedAudioDuration;
358
+ private resolveVoiceTextContent;
359
+ private findSourceTextClipForTtsAudio;
360
+ private resolveTtsAudioVoiceGenerationSource;
356
361
  private getVoiceLinkedAudioClips;
357
362
  private hasVoiceLinkedAudioClipsToRegenerate;
358
363
  private scheduleVoiceLinkedTextRegeneration;
@@ -37,7 +37,7 @@ export declare class HistoryManager {
37
37
  /**
38
38
  * 创建添加片段操作
39
39
  */
40
- createAddClipAction(clip: Clip): AddClipAction;
40
+ createAddClipAction(clip: Clip, targetTrackId?: string | null, targetTrackSnapshot?: Track | null, targetTrackRestoreAnchor?: TrackRestoreAnchor | null): AddClipAction;
41
41
  /**
42
42
  * 创建移除片段操作
43
43
  */
@@ -23,6 +23,7 @@ export declare class TimelineHistoryExecutor {
23
23
  private executeRedoInternal;
24
24
  private restoreMovedClip;
25
25
  private normalizeRemoveClipActionData;
26
+ private normalizeAddClipActionData;
26
27
  private resolveTrackIdForHistorySnapshot;
27
28
  private resolveRestoreAnchor;
28
29
  }
@@ -4,8 +4,8 @@ export declare class TimelineHistoryRecorder {
4
4
  private readonly history;
5
5
  private readonly transactionStack;
6
6
  constructor(history: HistoryManager);
7
- createAddClipAction(clip: Clip): Action;
8
- recordAddClip(clip: Clip): Action;
7
+ createAddClipAction(clip: Clip, targetTrackId?: string | null, targetTrackSnapshot?: Track | null, targetTrackRestoreAnchor?: TrackRestoreAnchor | null): Action;
8
+ recordAddClip(clip: Clip, targetTrackId?: string | null, targetTrackSnapshot?: Track | null, targetTrackRestoreAnchor?: TrackRestoreAnchor | null): Action;
9
9
  createRemoveClipAction(clip: Clip, sourceTrackId?: string | null, sourceTrackSnapshot?: Track | null, sourceTrackRestoreAnchor?: TrackRestoreAnchor | null): Action;
10
10
  recordRemoveClip(clip: Clip, sourceTrackId?: string | null, sourceTrackSnapshot?: Track | null, sourceTrackRestoreAnchor?: TrackRestoreAnchor | null): Action;
11
11
  recordClipUpdate(clip: Clip, originalClip?: Clip, clipUpdates?: ClipStateUpdate[]): Action | null;
@@ -9,6 +9,20 @@ export declare const ZOOM_PRESETS: {
9
9
  LARGE: number;
10
10
  MAX: number;
11
11
  };
12
+ export declare const DEFAULT_TIMELINE_DURATION_MS = 3600000;
13
+ export declare const ZOOM_GESTURE: {
14
+ SENSITIVITY_FACTOR: number;
15
+ CONVERGENCE_THRESHOLD: number;
16
+ };
17
+ export declare const DEFAULT_TEXT_CLIP_DURATION_MS = 3000;
18
+ export declare const VOICE_LINKED_TEXT_REGEN_DEBOUNCE_MS = 450;
19
+ export declare const SNAP_SCORING: {
20
+ ANCHOR_INDEX_WEIGHT: number;
21
+ };
22
+ export declare const TEXT_CLIP_FONT_SIZE_LIMITS: {
23
+ MIN: number;
24
+ MAX: number;
25
+ };
12
26
  export declare const ZOOM_ANIMATION: {
13
27
  DURATION: number;
14
28
  EASING: string;
@@ -58,6 +58,8 @@ export interface VoiceOption {
58
58
  audiofile?: string;
59
59
  source?: Record<string, unknown>;
60
60
  }
61
+ export declare function isAudioOnlyTtsClipCandidate(clip: Pick<ClipConfig, 'type' | 'textContent' | 'ttsVoiceId'> | Pick<ClipEntity, 'type' | 'textContent' | 'ttsVoiceId'> | Pick<Clip, 'type' | 'textContent' | 'ttsVoiceId'>): boolean;
62
+ export declare function isTtsAudioClip(clip: Pick<ClipConfig, 'type' | 'ttsSourceTextClipId' | 'textContent' | 'ttsVoiceId'> | Pick<ClipEntity, 'type' | 'ttsSourceTextClipId' | 'textContent' | 'ttsVoiceId'> | Pick<Clip, 'type' | 'ttsSourceTextClipId' | 'textContent' | 'ttsVoiceId'>): boolean;
61
63
  export interface GenerateVoiceRequest {
62
64
  requestId: string;
63
65
  sourceTextClipId: string;
@@ -270,9 +272,15 @@ export interface ClipStateUpdate {
270
272
  newState: Clip;
271
273
  previousState: Clip;
272
274
  }
275
+ export type AddClipActionData = Clip | {
276
+ clip: Clip;
277
+ targetTrackId: string | null;
278
+ targetTrackSnapshot: Track | null;
279
+ targetTrackRestoreAnchor: TrackRestoreAnchor | null;
280
+ };
273
281
  export interface AddClipAction {
274
282
  type: 'add_clip';
275
- data: Clip;
283
+ data: AddClipActionData;
276
284
  timestamp: number;
277
285
  }
278
286
  export interface RemoveClipAction {