xxf_react 0.5.3 → 0.5.4

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
@@ -13,3 +13,7 @@ bun run src/index.ts
13
13
  ```
14
14
 
15
15
  This project was created using `bun init` in bun v1.3.2. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
16
+
17
+ ## 文档
18
+
19
+ - [PlaybackQueue 播放队列状态管理](./src/media/playback-queue-store.md)
@@ -1,2 +1,3 @@
1
1
  export * from './video-state';
2
+ export * from './playback-queue-store';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAEA,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/media/index.ts"],"names":[],"mappings":"AAEA,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC"}
@@ -1,2 +1,3 @@
1
1
  'use client';
2
2
  export * from './video-state';
3
+ export * from './playback-queue-store';
@@ -0,0 +1,642 @@
1
+ /**
2
+ * @fileoverview 播放队列状态管理 (Zustand)
3
+ *
4
+ * 支持多 Tab / 多页面场景,全局只有一个视频在播放。
5
+ *
6
+ * @module playback-queue-store
7
+ * @see {@link ./playback-queue-store.md} 详细文档
8
+ */
9
+ /**
10
+ * Source 数据结构
11
+ *
12
+ * 每个 source 代表一个独立的播放列表,通常对应一个 Tab 或页面。
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const homeSource: SourceData = {
17
+ * itemIds: ['video-1', 'video-2', 'video-3']
18
+ * }
19
+ * ```
20
+ */
21
+ interface SourceData {
22
+ /**
23
+ * 该 source 下的所有项目 ID 列表
24
+ *
25
+ * - 有序数组,决定播放顺序
26
+ * - ID 应该是唯一的字符串标识符
27
+ */
28
+ itemIds: string[];
29
+ }
30
+ /**
31
+ * 播放队列状态
32
+ *
33
+ * 核心状态结构,包含所有 source 的数据和当前播放状态。
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const state: PlaybackQueueState = {
38
+ * sources: {
39
+ * 'home': { itemIds: ['a', 'b', 'c'] },
40
+ * 'community': { itemIds: ['d', 'e'] },
41
+ * },
42
+ * itemIdToSource: { 'a': 'home', 'b': 'home', 'd': 'community' },
43
+ * activeSource: 'home',
44
+ * currentId: 'a',
45
+ * muted: true,
46
+ * loop: true,
47
+ * }
48
+ * ```
49
+ */
50
+ interface PlaybackQueueState {
51
+ /**
52
+ * 多个 source 的数据
53
+ *
54
+ * key: source 名称(如 'home', 'community', 'detail-123')
55
+ * value: 该 source 的数据
56
+ */
57
+ sources: Record<string, SourceData>;
58
+ /**
59
+ * ID 到 source 的反向索引
60
+ *
61
+ * 用于 `playById` 快速查找某个 ID 属于哪个 source。
62
+ * 复杂度从 O(n*m) 降低到 O(1)。
63
+ *
64
+ * @internal 内部使用,自动维护
65
+ */
66
+ itemIdToSource: Record<string, string>;
67
+ /**
68
+ * 当前激活的 source 名称
69
+ *
70
+ * - 播放、next、prev 操作都在此 source 内进行
71
+ * - 为 null 表示没有激活的 source
72
+ */
73
+ activeSource: string | null;
74
+ /**
75
+ * 当前正在播放的项目 ID
76
+ *
77
+ * - 全局唯一,保证同一时间只有一个视频在播放
78
+ * - 为 null 表示没有视频在播放
79
+ */
80
+ currentId: string | null;
81
+ /**
82
+ * 是否静音
83
+ *
84
+ * @default true(默认静音,符合浏览器自动播放策略)
85
+ */
86
+ muted: boolean;
87
+ /**
88
+ * 是否循环播放
89
+ *
90
+ * - true: 播放到最后一个后自动回到第一个
91
+ * - false: 播放到最后一个后停止
92
+ *
93
+ * @default true
94
+ */
95
+ loop: boolean;
96
+ }
97
+ /**
98
+ * 播放队列操作方法接口
99
+ *
100
+ * 定义所有可用的操作方法。
101
+ */
102
+ interface PlaybackQueueActions {
103
+ /**
104
+ * 设置某个 source 的播放列表
105
+ *
106
+ * 用于初始化或更新某个 Tab/页面的播放列表。
107
+ *
108
+ * @param source - source 名称,建议使用有意义的标识符
109
+ * - 'home' - 首页
110
+ * - 'community' - 社区页
111
+ * - 'detail-{id}' - 详情页
112
+ * @param ids - 项目 ID 列表,会自动去重
113
+ * @param autoPlay - 是否自动播放第一个,默认 false
114
+ *
115
+ * @example
116
+ * ```tsx
117
+ * // 初始化首页列表
118
+ * const { setSource } = usePlaybackActions()
119
+ *
120
+ * useEffect(() => {
121
+ * setSource('home', templates.map(t => t.id))
122
+ * }, [templates])
123
+ *
124
+ * // 初始化并自动播放
125
+ * setSource('home', ['id-1', 'id-2'], true)
126
+ * ```
127
+ */
128
+ setSource: (source: string, ids: string[], autoPlay?: boolean) => void;
129
+ /**
130
+ * 向某个 source 追加项目
131
+ *
132
+ * 用于滚动加载场景,追加新数据时不影响当前播放状态。
133
+ *
134
+ * @param source - source 名称
135
+ * @param ids - 要追加的项目 ID 列表,会自动去重
136
+ *
137
+ * @example
138
+ * ```tsx
139
+ * // 滚动加载更多
140
+ * const { appendToSource } = usePlaybackActions()
141
+ *
142
+ * const onLoadMore = (newItems: Template[]) => {
143
+ * appendToSource('home', newItems.map(t => t.id))
144
+ * }
145
+ * ```
146
+ */
147
+ appendToSource: (source: string, ids: string[]) => void;
148
+ /**
149
+ * 移除某个 source
150
+ *
151
+ * 用于页面卸载时清理数据,防止内存泄漏。
152
+ * 如果移除的是当前激活的 source,会自动停止播放。
153
+ *
154
+ * @param source - 要移除的 source 名称
155
+ *
156
+ * @example
157
+ * ```tsx
158
+ * // 详情页卸载时清理
159
+ * useEffect(() => {
160
+ * setSource(`detail-${id}`, items)
161
+ * return () => removeSource(`detail-${id}`)
162
+ * }, [id])
163
+ * ```
164
+ */
165
+ removeSource: (source: string) => void;
166
+ /**
167
+ * 播放指定 source 中的指定项目
168
+ *
169
+ * 会自动切换 activeSource 到指定的 source。
170
+ *
171
+ * @param source - source 名称
172
+ * @param id - 项目 ID
173
+ *
174
+ * @example
175
+ * ```tsx
176
+ * const { play } = usePlaybackActions()
177
+ *
178
+ * // 点击卡片播放
179
+ * <div onClick={() => play('home', item.id)}>
180
+ * ...
181
+ * </div>
182
+ * ```
183
+ */
184
+ play: (source: string, id: string) => void;
185
+ /**
186
+ * 播放指定项目(自动查找 source)
187
+ *
188
+ * 优先在当前 activeSource 中查找,找不到则通过反向索引查找。
189
+ * 适用于不关心具体 source 的场景。
190
+ *
191
+ * @param id - 项目 ID
192
+ *
193
+ * @example
194
+ * ```tsx
195
+ * const { playById } = usePlaybackActions()
196
+ *
197
+ * // 从搜索结果播放(不知道属于哪个 source)
198
+ * <div onClick={() => playById(searchResult.id)}>
199
+ * ...
200
+ * </div>
201
+ * ```
202
+ */
203
+ playById: (id: string) => void;
204
+ /**
205
+ * 播放下一个
206
+ *
207
+ * 在当前 activeSource 内播放下一个项目。
208
+ * 如果已是最后一个:
209
+ * - loop=true: 回到第一个
210
+ * - loop=false: 停止播放(currentId 变为 null)
211
+ *
212
+ * @example
213
+ * ```tsx
214
+ * const { next } = usePlaybackActions()
215
+ *
216
+ * <button onClick={next}>下一个</button>
217
+ * ```
218
+ */
219
+ next: () => void;
220
+ /**
221
+ * 播放上一个
222
+ *
223
+ * 在当前 activeSource 内播放上一个项目。
224
+ * 如果已是第一个:
225
+ * - loop=true: 跳到最后一个
226
+ * - loop=false: 停止播放
227
+ *
228
+ * @example
229
+ * ```tsx
230
+ * const { prev } = usePlaybackActions()
231
+ *
232
+ * <button onClick={prev}>上一个</button>
233
+ * ```
234
+ */
235
+ prev: () => void;
236
+ /**
237
+ * 停止播放
238
+ *
239
+ * 将 currentId 设为 null,但保留 activeSource。
240
+ *
241
+ * @example
242
+ * ```tsx
243
+ * const { stop } = usePlaybackActions()
244
+ *
245
+ * <button onClick={stop}>停止</button>
246
+ * ```
247
+ */
248
+ stop: () => void;
249
+ /**
250
+ * 播放完成回调
251
+ *
252
+ * 应在视频的 onEnded 事件中调用,会自动播放下一个。
253
+ *
254
+ * @example
255
+ * ```tsx
256
+ * const { onEnded } = usePlaybackActions()
257
+ *
258
+ * <video onEnded={onEnded} />
259
+ * ```
260
+ */
261
+ onEnded: () => void;
262
+ /**
263
+ * 设置静音状态
264
+ *
265
+ * @param muted - true 静音,false 取消静音
266
+ *
267
+ * @example
268
+ * ```tsx
269
+ * const { setMuted } = usePlaybackActions()
270
+ *
271
+ * setMuted(true) // 静音
272
+ * setMuted(false) // 取消静音
273
+ * ```
274
+ */
275
+ setMuted: (muted: boolean) => void;
276
+ /**
277
+ * 切换静音状态
278
+ *
279
+ * @example
280
+ * ```tsx
281
+ * const { toggleMute } = usePlaybackActions()
282
+ * const muted = usePlaybackMuted()
283
+ *
284
+ * <button onClick={toggleMute}>
285
+ * {muted ? '🔇' : '🔊'}
286
+ * </button>
287
+ * ```
288
+ */
289
+ toggleMute: () => void;
290
+ /**
291
+ * 设置循环播放
292
+ *
293
+ * @param loop - true 循环播放,false 播完停止
294
+ *
295
+ * @example
296
+ * ```tsx
297
+ * const { setLoop } = usePlaybackActions()
298
+ *
299
+ * setLoop(false) // 关闭循环
300
+ * ```
301
+ */
302
+ setLoop: (loop: boolean) => void;
303
+ /**
304
+ * 重置所有状态
305
+ *
306
+ * 将状态恢复到初始值,清空所有 source 和播放状态。
307
+ * 通常用于用户登出或需要完全重置的场景。
308
+ *
309
+ * @example
310
+ * ```tsx
311
+ * const { reset } = usePlaybackActions()
312
+ *
313
+ * // 用户登出时清理
314
+ * const handleLogout = () => {
315
+ * reset()
316
+ * // ...其他清理逻辑
317
+ * }
318
+ * ```
319
+ */
320
+ reset: () => void;
321
+ }
322
+ /**
323
+ * 播放队列 Store 完整类型
324
+ *
325
+ * 包含状态和操作方法。
326
+ */
327
+ type PlaybackQueueStore = PlaybackQueueState & PlaybackQueueActions;
328
+ /**
329
+ * 播放队列 Zustand Store
330
+ *
331
+ * 使用 `subscribeWithSelector` 中间件支持选择性订阅。
332
+ *
333
+ * @example
334
+ * ```tsx
335
+ * // 直接使用 store(不推荐,建议使用封装的 hooks)
336
+ * const currentId = usePlaybackQueueStore(state => state.currentId)
337
+ * ```
338
+ */
339
+ export declare const usePlaybackQueueStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<PlaybackQueueStore>, "subscribe"> & {
340
+ subscribe: {
341
+ (listener: (selectedState: PlaybackQueueStore, previousSelectedState: PlaybackQueueStore) => void): () => void;
342
+ <U>(selector: (state: PlaybackQueueStore) => U, listener: (selectedState: U, previousSelectedState: U) => void, options?: {
343
+ equalityFn?: ((a: U, b: U) => boolean) | undefined;
344
+ fireImmediately?: boolean;
345
+ } | undefined): () => void;
346
+ };
347
+ }>;
348
+ /**
349
+ * 获取播放队列操作方法
350
+ *
351
+ * 使用 shallow 比较,避免不必要的重渲染。
352
+ * 返回的方法引用稳定,可以安全地用作依赖项。
353
+ *
354
+ * @returns 所有操作方法
355
+ *
356
+ * @example
357
+ * ```tsx
358
+ * const { play, stop, next, prev, toggleMute, onEnded } = usePlaybackActions()
359
+ *
360
+ * // 在视频组件中
361
+ * <video
362
+ * onEnded={onEnded}
363
+ * onClick={() => play('home', item.id)}
364
+ * />
365
+ *
366
+ * // 控制按钮
367
+ * <button onClick={prev}>上一个</button>
368
+ * <button onClick={next}>下一个</button>
369
+ * <button onClick={toggleMute}>静音</button>
370
+ * ```
371
+ */
372
+ export declare function usePlaybackActions(): PlaybackQueueActions;
373
+ /**
374
+ * 判断指定项目是否正在播放
375
+ *
376
+ * 选择性订阅,只有当该项目的播放状态变化时才会重渲染。
377
+ * 这是性能最优的播放状态判断方式。
378
+ *
379
+ * @param id - 项目 ID
380
+ * @returns 是否正在播放
381
+ *
382
+ * @example
383
+ * ```tsx
384
+ * const VideoCard = ({ item }) => {
385
+ * const isPlaying = usePlaybackIsPlaying(item.id)
386
+ *
387
+ * return (
388
+ * <video autoPlay={isPlaying} muted />
389
+ * )
390
+ * }
391
+ * ```
392
+ */
393
+ export declare function usePlaybackIsPlaying(id: string): boolean;
394
+ /**
395
+ * 获取静音状态
396
+ *
397
+ * @returns 是否静音
398
+ *
399
+ * @example
400
+ * ```tsx
401
+ * const muted = usePlaybackMuted()
402
+ *
403
+ * <video muted={muted} />
404
+ * ```
405
+ */
406
+ export declare function usePlaybackMuted(): boolean;
407
+ /**
408
+ * 获取当前播放的项目 ID
409
+ *
410
+ * @returns 当前播放的 ID,没有播放时为 null
411
+ *
412
+ * @example
413
+ * ```tsx
414
+ * const currentId = usePlaybackCurrentId()
415
+ *
416
+ * if (currentId) {
417
+ * console.log('正在播放:', currentId)
418
+ * }
419
+ * ```
420
+ */
421
+ export declare function usePlaybackCurrentId(): string | null;
422
+ /**
423
+ * 获取当前激活的 source 名称
424
+ *
425
+ * @returns source 名称,没有激活时为 null
426
+ *
427
+ * @example
428
+ * ```tsx
429
+ * const activeSource = usePlaybackActiveSource()
430
+ *
431
+ * // 高亮当前激活的 Tab
432
+ * <Tab active={activeSource === 'home'}>首页</Tab>
433
+ * ```
434
+ */
435
+ export declare function usePlaybackActiveSource(): string | null;
436
+ /**
437
+ * 获取循环播放状态
438
+ *
439
+ * @returns 是否循环播放
440
+ *
441
+ * @example
442
+ * ```tsx
443
+ * const loop = usePlaybackLoop()
444
+ * const { setLoop } = usePlaybackActions()
445
+ *
446
+ * <button onClick={() => setLoop(!loop)}>
447
+ * {loop ? '循环开' : '循环关'}
448
+ * </button>
449
+ * ```
450
+ */
451
+ export declare function usePlaybackLoop(): boolean;
452
+ /**
453
+ * 获取指定 source 的项目 ID 列表
454
+ *
455
+ * @param source - source 名称
456
+ * @returns 项目 ID 列表,source 不存在时返回空数组
457
+ *
458
+ * @example
459
+ * ```tsx
460
+ * const homeItems = usePlaybackSourceItems('home')
461
+ *
462
+ * console.log('首页有', homeItems.length, '个视频')
463
+ * ```
464
+ */
465
+ export declare function usePlaybackSourceItems(source: string): string[];
466
+ /**
467
+ * 获取单个 item 的播放状态和静音状态(常用组合)
468
+ *
469
+ * 使用 shallow 比较,仅当 isPlaying 或 muted 变化时重渲染。
470
+ *
471
+ * @param id - 项目 ID
472
+ * @returns { isPlaying: boolean, muted: boolean }
473
+ *
474
+ * @example
475
+ * ```tsx
476
+ * const VideoCard = ({ item }) => {
477
+ * const { isPlaying, muted } = usePlaybackItemState(item.id)
478
+ * const { onEnded } = usePlaybackActions()
479
+ *
480
+ * return (
481
+ * <video
482
+ * autoPlay={isPlaying}
483
+ * muted={muted}
484
+ * onEnded={onEnded}
485
+ * />
486
+ * )
487
+ * }
488
+ * ```
489
+ */
490
+ export declare function usePlaybackItemState(id: string): {
491
+ isPlaying: boolean;
492
+ muted: boolean;
493
+ };
494
+ /**
495
+ * 播放队列命名空间
496
+ *
497
+ * 非 Hook 版本的 API,用于:
498
+ * - 组件外部调用(如事件回调、工具函数)
499
+ * - 不在 React 组件中的场景
500
+ * - 需要命令式调用的场景
501
+ *
502
+ * @example
503
+ * ```tsx
504
+ * // 在事件回调中使用
505
+ * const handleClick = () => {
506
+ * PlaybackQueue.play('home', 'item-1')
507
+ * }
508
+ *
509
+ * // 在非组件代码中使用
510
+ * PlaybackQueue.toggleMute()
511
+ *
512
+ * // 获取当前状态快照
513
+ * const state = PlaybackQueue.getState()
514
+ * console.log('当前播放:', state.currentId)
515
+ *
516
+ * // 订阅状态变化
517
+ * const unsubscribe = PlaybackQueue.subscribeCurrentId((currentId, prevId) => {
518
+ * console.log('播放变化:', prevId, '->', currentId)
519
+ * })
520
+ * // 不再需要时取消订阅
521
+ * unsubscribe()
522
+ * ```
523
+ */
524
+ export declare const PlaybackQueue: {
525
+ /**
526
+ * 获取当前状态快照(非响应式)
527
+ *
528
+ * @returns 当前完整状态
529
+ */
530
+ readonly getState: () => PlaybackQueueState;
531
+ /**
532
+ * 设置某个 source 的播放列表
533
+ *
534
+ * @see {@link PlaybackQueueActions.setSource}
535
+ */
536
+ readonly setSource: (source: string, ids: string[], autoPlay?: boolean | undefined) => void;
537
+ /**
538
+ * 向某个 source 追加项目
539
+ *
540
+ * @see {@link PlaybackQueueActions.appendToSource}
541
+ */
542
+ readonly appendToSource: (source: string, ids: string[]) => void;
543
+ /**
544
+ * 移除某个 source
545
+ *
546
+ * @see {@link PlaybackQueueActions.removeSource}
547
+ */
548
+ readonly removeSource: (source: string) => void;
549
+ /**
550
+ * 播放指定项目
551
+ *
552
+ * @see {@link PlaybackQueueActions.play}
553
+ */
554
+ readonly play: (source: string, id: string) => void;
555
+ /**
556
+ * 播放指定项目(自动查找 source)
557
+ *
558
+ * @see {@link PlaybackQueueActions.playById}
559
+ */
560
+ readonly playById: (id: string) => void;
561
+ /**
562
+ * 播放下一个
563
+ *
564
+ * @see {@link PlaybackQueueActions.next}
565
+ */
566
+ readonly next: () => void;
567
+ /**
568
+ * 播放上一个
569
+ *
570
+ * @see {@link PlaybackQueueActions.prev}
571
+ */
572
+ readonly prev: () => void;
573
+ /**
574
+ * 停止播放
575
+ *
576
+ * @see {@link PlaybackQueueActions.stop}
577
+ */
578
+ readonly stop: () => void;
579
+ /**
580
+ * 播放完成回调
581
+ *
582
+ * @see {@link PlaybackQueueActions.onEnded}
583
+ */
584
+ readonly onEnded: () => void;
585
+ /**
586
+ * 设置静音状态
587
+ *
588
+ * @see {@link PlaybackQueueActions.setMuted}
589
+ */
590
+ readonly setMuted: (muted: boolean) => void;
591
+ /**
592
+ * 切换静音状态
593
+ *
594
+ * @see {@link PlaybackQueueActions.toggleMute}
595
+ */
596
+ readonly toggleMute: () => void;
597
+ /**
598
+ * 设置循环播放
599
+ *
600
+ * @see {@link PlaybackQueueActions.setLoop}
601
+ */
602
+ readonly setLoop: (loop: boolean) => void;
603
+ /**
604
+ * 重置所有状态
605
+ *
606
+ * @see {@link PlaybackQueueActions.reset}
607
+ */
608
+ readonly reset: () => void;
609
+ /**
610
+ * 订阅当前播放 ID 变化
611
+ *
612
+ * @param callback - 回调函数,参数为 (新值, 旧值)
613
+ * @returns 取消订阅函数
614
+ *
615
+ * @example
616
+ * ```ts
617
+ * const unsubscribe = PlaybackQueue.subscribeCurrentId((currentId, prevId) => {
618
+ * console.log('播放变化:', prevId, '->', currentId)
619
+ * })
620
+ *
621
+ * // 清理时取消订阅
622
+ * unsubscribe()
623
+ * ```
624
+ */
625
+ readonly subscribeCurrentId: (callback: (currentId: string | null, prevId: string | null) => void) => () => void;
626
+ /**
627
+ * 订阅 activeSource 变化
628
+ *
629
+ * @param callback - 回调函数,参数为 (新值, 旧值)
630
+ * @returns 取消订阅函数
631
+ */
632
+ readonly subscribeActiveSource: (callback: (source: string | null, prevSource: string | null) => void) => () => void;
633
+ /**
634
+ * 订阅静音状态变化
635
+ *
636
+ * @param callback - 回调函数,参数为 (新值, 旧值)
637
+ * @returns 取消订阅函数
638
+ */
639
+ readonly subscribeMuted: (callback: (muted: boolean, prevMuted: boolean) => void) => () => void;
640
+ };
641
+ export {};
642
+ //# sourceMappingURL=playback-queue-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playback-queue-store.d.ts","sourceRoot":"","sources":["../../src/media/playback-queue-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,614 @@
1
+ /**
2
+ * @fileoverview 播放队列状态管理 (Zustand)
3
+ *
4
+ * 支持多 Tab / 多页面场景,全局只有一个视频在播放。
5
+ *
6
+ * @module playback-queue-store
7
+ * @see {@link ./playback-queue-store.md} 详细文档
8
+ */
9
+ import { create } from 'zustand';
10
+ import { subscribeWithSelector } from 'zustand/middleware';
11
+ import { useShallow } from 'zustand/react/shallow';
12
+ // ============================================================================
13
+ // Initial State - 初始状态
14
+ // ============================================================================
15
+ /**
16
+ * 初始状态
17
+ *
18
+ * @internal
19
+ */
20
+ const initialState = {
21
+ sources: {},
22
+ itemIdToSource: {},
23
+ activeSource: null,
24
+ currentId: null,
25
+ muted: true, // 默认静音,符合浏览器自动播放策略
26
+ loop: true, // 默认循环播放
27
+ };
28
+ // ============================================================================
29
+ // Helper Functions - 辅助函数
30
+ // ============================================================================
31
+ /**
32
+ * 更新单个 source 的反向索引
33
+ *
34
+ * @param currentIndex - 当前的反向索引
35
+ * @param sourceName - source 名称
36
+ * @param oldIds - 旧的 ID 列表
37
+ * @param newIds - 新的 ID 列表
38
+ * @returns 更新后的反向索引
39
+ *
40
+ * @internal
41
+ */
42
+ function updateReverseIndexForSource(currentIndex, sourceName, oldIds, newIds) {
43
+ const index = { ...currentIndex };
44
+ // 移除旧的映射
45
+ for (const id of oldIds) {
46
+ if (index[id] === sourceName) {
47
+ delete index[id];
48
+ }
49
+ }
50
+ // 添加新的映射
51
+ for (const id of newIds) {
52
+ index[id] = sourceName;
53
+ }
54
+ return index;
55
+ }
56
+ // ============================================================================
57
+ // Store - Zustand Store 定义
58
+ // ============================================================================
59
+ /**
60
+ * 播放队列 Zustand Store
61
+ *
62
+ * 使用 `subscribeWithSelector` 中间件支持选择性订阅。
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * // 直接使用 store(不推荐,建议使用封装的 hooks)
67
+ * const currentId = usePlaybackQueueStore(state => state.currentId)
68
+ * ```
69
+ */
70
+ export const usePlaybackQueueStore = create()(subscribeWithSelector((set, get) => ({
71
+ ...initialState,
72
+ setSource: (source, ids, autoPlay = false) => {
73
+ if (!source) {
74
+ if (process.env.NODE_ENV === 'development') {
75
+ console.warn('[PlaybackQueue] source name is required');
76
+ }
77
+ return;
78
+ }
79
+ const uniqueIds = [...new Set(ids || [])];
80
+ set(state => {
81
+ var _a;
82
+ const oldIds = ((_a = state.sources[source]) === null || _a === void 0 ? void 0 : _a.itemIds) || [];
83
+ const newSources = {
84
+ ...state.sources,
85
+ [source]: { itemIds: uniqueIds },
86
+ };
87
+ const newIndex = updateReverseIndexForSource(state.itemIdToSource, source, oldIds, uniqueIds);
88
+ // 检查当前播放的项目是否还在列表中
89
+ let newCurrentId = state.currentId;
90
+ let newActiveSource = state.activeSource;
91
+ if (state.activeSource === source && state.currentId) {
92
+ if (!uniqueIds.includes(state.currentId)) {
93
+ // 当前播放的项目不在新列表中
94
+ newCurrentId = autoPlay && uniqueIds.length > 0 ? uniqueIds[0] : null;
95
+ }
96
+ }
97
+ // 如果要自动播放且当前没有在播放
98
+ if (autoPlay && uniqueIds.length > 0 && !state.currentId) {
99
+ newActiveSource = source;
100
+ newCurrentId = uniqueIds[0];
101
+ }
102
+ return {
103
+ sources: newSources,
104
+ itemIdToSource: newIndex,
105
+ activeSource: newActiveSource,
106
+ currentId: newCurrentId,
107
+ };
108
+ });
109
+ },
110
+ appendToSource: (source, ids) => {
111
+ if (!source || !ids || ids.length === 0)
112
+ return;
113
+ set(state => {
114
+ const sourceData = state.sources[source];
115
+ const existingIds = (sourceData === null || sourceData === void 0 ? void 0 : sourceData.itemIds) || [];
116
+ const existingSet = new Set(existingIds);
117
+ const newIds = ids.filter(id => !existingSet.has(id));
118
+ if (newIds.length === 0)
119
+ return state;
120
+ const allIds = [...existingIds, ...newIds];
121
+ const newIndex = { ...state.itemIdToSource };
122
+ for (const id of newIds) {
123
+ newIndex[id] = source;
124
+ }
125
+ return {
126
+ sources: {
127
+ ...state.sources,
128
+ [source]: { itemIds: allIds },
129
+ },
130
+ itemIdToSource: newIndex,
131
+ };
132
+ });
133
+ },
134
+ removeSource: (source) => {
135
+ set(state => {
136
+ const sourceData = state.sources[source];
137
+ if (!sourceData)
138
+ return state;
139
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
140
+ const { [source]: _removed, ...restSources } = state.sources;
141
+ // 清理反向索引
142
+ const newIndex = { ...state.itemIdToSource };
143
+ for (const id of sourceData.itemIds) {
144
+ if (newIndex[id] === source) {
145
+ delete newIndex[id];
146
+ }
147
+ }
148
+ // 如果移除的是当前激活的 source,停止播放
149
+ if (state.activeSource === source) {
150
+ return {
151
+ sources: restSources,
152
+ itemIdToSource: newIndex,
153
+ activeSource: null,
154
+ currentId: null,
155
+ };
156
+ }
157
+ return {
158
+ sources: restSources,
159
+ itemIdToSource: newIndex,
160
+ };
161
+ });
162
+ },
163
+ play: (source, id) => {
164
+ const { sources } = get();
165
+ const sourceData = sources[source];
166
+ if (!sourceData) {
167
+ if (process.env.NODE_ENV === 'development') {
168
+ console.warn(`[PlaybackQueue] Source "${source}" not found`);
169
+ }
170
+ return;
171
+ }
172
+ if (!sourceData.itemIds.includes(id)) {
173
+ if (process.env.NODE_ENV === 'development') {
174
+ console.warn(`[PlaybackQueue] Item "${id}" not found in source "${source}"`);
175
+ }
176
+ return;
177
+ }
178
+ set({
179
+ activeSource: source,
180
+ currentId: id,
181
+ });
182
+ },
183
+ playById: (id) => {
184
+ var _a;
185
+ const { sources, activeSource, itemIdToSource } = get();
186
+ // 优先检查当前 activeSource
187
+ if (activeSource && ((_a = sources[activeSource]) === null || _a === void 0 ? void 0 : _a.itemIds.includes(id))) {
188
+ set({ currentId: id });
189
+ return;
190
+ }
191
+ // 使用反向索引快速查找
192
+ const source = itemIdToSource[id];
193
+ if (!source) {
194
+ if (process.env.NODE_ENV === 'development') {
195
+ console.warn(`[PlaybackQueue] Item "${id}" not found in any source`);
196
+ }
197
+ return;
198
+ }
199
+ set({
200
+ activeSource: source,
201
+ currentId: id,
202
+ });
203
+ },
204
+ next: () => {
205
+ const { sources, activeSource, currentId, loop } = get();
206
+ if (!activeSource)
207
+ return;
208
+ const sourceData = sources[activeSource];
209
+ if (!sourceData || sourceData.itemIds.length === 0)
210
+ return;
211
+ // 如果没有当前播放项,播放第一个
212
+ if (!currentId) {
213
+ set({ currentId: sourceData.itemIds[0] });
214
+ return;
215
+ }
216
+ const currentIndex = sourceData.itemIds.indexOf(currentId);
217
+ if (currentIndex === -1) {
218
+ set({ currentId: sourceData.itemIds[0] });
219
+ return;
220
+ }
221
+ const nextIndex = currentIndex + 1;
222
+ if (nextIndex >= sourceData.itemIds.length) {
223
+ // 已是最后一个
224
+ set({ currentId: loop ? sourceData.itemIds[0] : null });
225
+ return;
226
+ }
227
+ set({ currentId: sourceData.itemIds[nextIndex] });
228
+ },
229
+ prev: () => {
230
+ const { sources, activeSource, currentId, loop } = get();
231
+ if (!activeSource)
232
+ return;
233
+ const sourceData = sources[activeSource];
234
+ if (!sourceData || sourceData.itemIds.length === 0)
235
+ return;
236
+ if (!currentId) {
237
+ set({ currentId: sourceData.itemIds[sourceData.itemIds.length - 1] });
238
+ return;
239
+ }
240
+ const currentIndex = sourceData.itemIds.indexOf(currentId);
241
+ if (currentIndex === -1) {
242
+ set({ currentId: sourceData.itemIds[0] });
243
+ return;
244
+ }
245
+ const prevIndex = currentIndex - 1;
246
+ if (prevIndex < 0) {
247
+ set({
248
+ currentId: loop ? sourceData.itemIds[sourceData.itemIds.length - 1] : null
249
+ });
250
+ return;
251
+ }
252
+ set({ currentId: sourceData.itemIds[prevIndex] });
253
+ },
254
+ stop: () => {
255
+ set({ currentId: null });
256
+ },
257
+ onEnded: () => {
258
+ get().next();
259
+ },
260
+ setMuted: (muted) => {
261
+ set({ muted });
262
+ },
263
+ toggleMute: () => {
264
+ set(state => ({ muted: !state.muted }));
265
+ },
266
+ setLoop: (loop) => {
267
+ set({ loop });
268
+ },
269
+ reset: () => {
270
+ set(initialState);
271
+ },
272
+ })));
273
+ // ============================================================================
274
+ // Selectors - 选择器(静态引用,避免重复创建)
275
+ // ============================================================================
276
+ /** @internal */
277
+ const selectActions = (state) => ({
278
+ setSource: state.setSource,
279
+ appendToSource: state.appendToSource,
280
+ removeSource: state.removeSource,
281
+ play: state.play,
282
+ playById: state.playById,
283
+ next: state.next,
284
+ prev: state.prev,
285
+ stop: state.stop,
286
+ onEnded: state.onEnded,
287
+ setMuted: state.setMuted,
288
+ toggleMute: state.toggleMute,
289
+ setLoop: state.setLoop,
290
+ reset: state.reset,
291
+ });
292
+ /** @internal */
293
+ const selectMuted = (state) => state.muted;
294
+ /** @internal */
295
+ const selectCurrentId = (state) => state.currentId;
296
+ /** @internal */
297
+ const selectActiveSource = (state) => state.activeSource;
298
+ /** @internal */
299
+ const selectLoop = (state) => state.loop;
300
+ // ============================================================================
301
+ // Hooks - React Hooks(选择性订阅,性能优化)
302
+ // ============================================================================
303
+ /**
304
+ * 获取播放队列操作方法
305
+ *
306
+ * 使用 shallow 比较,避免不必要的重渲染。
307
+ * 返回的方法引用稳定,可以安全地用作依赖项。
308
+ *
309
+ * @returns 所有操作方法
310
+ *
311
+ * @example
312
+ * ```tsx
313
+ * const { play, stop, next, prev, toggleMute, onEnded } = usePlaybackActions()
314
+ *
315
+ * // 在视频组件中
316
+ * <video
317
+ * onEnded={onEnded}
318
+ * onClick={() => play('home', item.id)}
319
+ * />
320
+ *
321
+ * // 控制按钮
322
+ * <button onClick={prev}>上一个</button>
323
+ * <button onClick={next}>下一个</button>
324
+ * <button onClick={toggleMute}>静音</button>
325
+ * ```
326
+ */
327
+ export function usePlaybackActions() {
328
+ return usePlaybackQueueStore(useShallow(selectActions));
329
+ }
330
+ /**
331
+ * 判断指定项目是否正在播放
332
+ *
333
+ * 选择性订阅,只有当该项目的播放状态变化时才会重渲染。
334
+ * 这是性能最优的播放状态判断方式。
335
+ *
336
+ * @param id - 项目 ID
337
+ * @returns 是否正在播放
338
+ *
339
+ * @example
340
+ * ```tsx
341
+ * const VideoCard = ({ item }) => {
342
+ * const isPlaying = usePlaybackIsPlaying(item.id)
343
+ *
344
+ * return (
345
+ * <video autoPlay={isPlaying} muted />
346
+ * )
347
+ * }
348
+ * ```
349
+ */
350
+ export function usePlaybackIsPlaying(id) {
351
+ return usePlaybackQueueStore(state => state.currentId === id);
352
+ }
353
+ /**
354
+ * 获取静音状态
355
+ *
356
+ * @returns 是否静音
357
+ *
358
+ * @example
359
+ * ```tsx
360
+ * const muted = usePlaybackMuted()
361
+ *
362
+ * <video muted={muted} />
363
+ * ```
364
+ */
365
+ export function usePlaybackMuted() {
366
+ return usePlaybackQueueStore(selectMuted);
367
+ }
368
+ /**
369
+ * 获取当前播放的项目 ID
370
+ *
371
+ * @returns 当前播放的 ID,没有播放时为 null
372
+ *
373
+ * @example
374
+ * ```tsx
375
+ * const currentId = usePlaybackCurrentId()
376
+ *
377
+ * if (currentId) {
378
+ * console.log('正在播放:', currentId)
379
+ * }
380
+ * ```
381
+ */
382
+ export function usePlaybackCurrentId() {
383
+ return usePlaybackQueueStore(selectCurrentId);
384
+ }
385
+ /**
386
+ * 获取当前激活的 source 名称
387
+ *
388
+ * @returns source 名称,没有激活时为 null
389
+ *
390
+ * @example
391
+ * ```tsx
392
+ * const activeSource = usePlaybackActiveSource()
393
+ *
394
+ * // 高亮当前激活的 Tab
395
+ * <Tab active={activeSource === 'home'}>首页</Tab>
396
+ * ```
397
+ */
398
+ export function usePlaybackActiveSource() {
399
+ return usePlaybackQueueStore(selectActiveSource);
400
+ }
401
+ /**
402
+ * 获取循环播放状态
403
+ *
404
+ * @returns 是否循环播放
405
+ *
406
+ * @example
407
+ * ```tsx
408
+ * const loop = usePlaybackLoop()
409
+ * const { setLoop } = usePlaybackActions()
410
+ *
411
+ * <button onClick={() => setLoop(!loop)}>
412
+ * {loop ? '循环开' : '循环关'}
413
+ * </button>
414
+ * ```
415
+ */
416
+ export function usePlaybackLoop() {
417
+ return usePlaybackQueueStore(selectLoop);
418
+ }
419
+ /**
420
+ * 获取指定 source 的项目 ID 列表
421
+ *
422
+ * @param source - source 名称
423
+ * @returns 项目 ID 列表,source 不存在时返回空数组
424
+ *
425
+ * @example
426
+ * ```tsx
427
+ * const homeItems = usePlaybackSourceItems('home')
428
+ *
429
+ * console.log('首页有', homeItems.length, '个视频')
430
+ * ```
431
+ */
432
+ export function usePlaybackSourceItems(source) {
433
+ return usePlaybackQueueStore(state => { var _a, _b; return (_b = (_a = state.sources[source]) === null || _a === void 0 ? void 0 : _a.itemIds) !== null && _b !== void 0 ? _b : []; });
434
+ }
435
+ /**
436
+ * 获取单个 item 的播放状态和静音状态(常用组合)
437
+ *
438
+ * 使用 shallow 比较,仅当 isPlaying 或 muted 变化时重渲染。
439
+ *
440
+ * @param id - 项目 ID
441
+ * @returns { isPlaying: boolean, muted: boolean }
442
+ *
443
+ * @example
444
+ * ```tsx
445
+ * const VideoCard = ({ item }) => {
446
+ * const { isPlaying, muted } = usePlaybackItemState(item.id)
447
+ * const { onEnded } = usePlaybackActions()
448
+ *
449
+ * return (
450
+ * <video
451
+ * autoPlay={isPlaying}
452
+ * muted={muted}
453
+ * onEnded={onEnded}
454
+ * />
455
+ * )
456
+ * }
457
+ * ```
458
+ */
459
+ export function usePlaybackItemState(id) {
460
+ return usePlaybackQueueStore(useShallow(state => ({
461
+ isPlaying: state.currentId === id,
462
+ muted: state.muted,
463
+ })));
464
+ }
465
+ // ============================================================================
466
+ // Non-Hook API - 非 Hook API(用于组件外部或副作用)
467
+ // ============================================================================
468
+ /**
469
+ * 播放队列命名空间
470
+ *
471
+ * 非 Hook 版本的 API,用于:
472
+ * - 组件外部调用(如事件回调、工具函数)
473
+ * - 不在 React 组件中的场景
474
+ * - 需要命令式调用的场景
475
+ *
476
+ * @example
477
+ * ```tsx
478
+ * // 在事件回调中使用
479
+ * const handleClick = () => {
480
+ * PlaybackQueue.play('home', 'item-1')
481
+ * }
482
+ *
483
+ * // 在非组件代码中使用
484
+ * PlaybackQueue.toggleMute()
485
+ *
486
+ * // 获取当前状态快照
487
+ * const state = PlaybackQueue.getState()
488
+ * console.log('当前播放:', state.currentId)
489
+ *
490
+ * // 订阅状态变化
491
+ * const unsubscribe = PlaybackQueue.subscribeCurrentId((currentId, prevId) => {
492
+ * console.log('播放变化:', prevId, '->', currentId)
493
+ * })
494
+ * // 不再需要时取消订阅
495
+ * unsubscribe()
496
+ * ```
497
+ */
498
+ export const PlaybackQueue = {
499
+ /**
500
+ * 获取当前状态快照(非响应式)
501
+ *
502
+ * @returns 当前完整状态
503
+ */
504
+ getState: () => usePlaybackQueueStore.getState(),
505
+ /**
506
+ * 设置某个 source 的播放列表
507
+ *
508
+ * @see {@link PlaybackQueueActions.setSource}
509
+ */
510
+ setSource: (...args) => usePlaybackQueueStore.getState().setSource(...args),
511
+ /**
512
+ * 向某个 source 追加项目
513
+ *
514
+ * @see {@link PlaybackQueueActions.appendToSource}
515
+ */
516
+ appendToSource: (...args) => usePlaybackQueueStore.getState().appendToSource(...args),
517
+ /**
518
+ * 移除某个 source
519
+ *
520
+ * @see {@link PlaybackQueueActions.removeSource}
521
+ */
522
+ removeSource: (...args) => usePlaybackQueueStore.getState().removeSource(...args),
523
+ /**
524
+ * 播放指定项目
525
+ *
526
+ * @see {@link PlaybackQueueActions.play}
527
+ */
528
+ play: (...args) => usePlaybackQueueStore.getState().play(...args),
529
+ /**
530
+ * 播放指定项目(自动查找 source)
531
+ *
532
+ * @see {@link PlaybackQueueActions.playById}
533
+ */
534
+ playById: (...args) => usePlaybackQueueStore.getState().playById(...args),
535
+ /**
536
+ * 播放下一个
537
+ *
538
+ * @see {@link PlaybackQueueActions.next}
539
+ */
540
+ next: () => usePlaybackQueueStore.getState().next(),
541
+ /**
542
+ * 播放上一个
543
+ *
544
+ * @see {@link PlaybackQueueActions.prev}
545
+ */
546
+ prev: () => usePlaybackQueueStore.getState().prev(),
547
+ /**
548
+ * 停止播放
549
+ *
550
+ * @see {@link PlaybackQueueActions.stop}
551
+ */
552
+ stop: () => usePlaybackQueueStore.getState().stop(),
553
+ /**
554
+ * 播放完成回调
555
+ *
556
+ * @see {@link PlaybackQueueActions.onEnded}
557
+ */
558
+ onEnded: () => usePlaybackQueueStore.getState().onEnded(),
559
+ /**
560
+ * 设置静音状态
561
+ *
562
+ * @see {@link PlaybackQueueActions.setMuted}
563
+ */
564
+ setMuted: (...args) => usePlaybackQueueStore.getState().setMuted(...args),
565
+ /**
566
+ * 切换静音状态
567
+ *
568
+ * @see {@link PlaybackQueueActions.toggleMute}
569
+ */
570
+ toggleMute: () => usePlaybackQueueStore.getState().toggleMute(),
571
+ /**
572
+ * 设置循环播放
573
+ *
574
+ * @see {@link PlaybackQueueActions.setLoop}
575
+ */
576
+ setLoop: (...args) => usePlaybackQueueStore.getState().setLoop(...args),
577
+ /**
578
+ * 重置所有状态
579
+ *
580
+ * @see {@link PlaybackQueueActions.reset}
581
+ */
582
+ reset: () => usePlaybackQueueStore.getState().reset(),
583
+ /**
584
+ * 订阅当前播放 ID 变化
585
+ *
586
+ * @param callback - 回调函数,参数为 (新值, 旧值)
587
+ * @returns 取消订阅函数
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * const unsubscribe = PlaybackQueue.subscribeCurrentId((currentId, prevId) => {
592
+ * console.log('播放变化:', prevId, '->', currentId)
593
+ * })
594
+ *
595
+ * // 清理时取消订阅
596
+ * unsubscribe()
597
+ * ```
598
+ */
599
+ subscribeCurrentId: (callback) => usePlaybackQueueStore.subscribe(state => state.currentId, callback),
600
+ /**
601
+ * 订阅 activeSource 变化
602
+ *
603
+ * @param callback - 回调函数,参数为 (新值, 旧值)
604
+ * @returns 取消订阅函数
605
+ */
606
+ subscribeActiveSource: (callback) => usePlaybackQueueStore.subscribe(state => state.activeSource, callback),
607
+ /**
608
+ * 订阅静音状态变化
609
+ *
610
+ * @param callback - 回调函数,参数为 (新值, 旧值)
611
+ * @returns 取消订阅函数
612
+ */
613
+ subscribeMuted: (callback) => usePlaybackQueueStore.subscribe(state => state.muted, callback),
614
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xxf_react",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -65,6 +65,7 @@
65
65
  "react": ">=18"
66
66
  },
67
67
  "dependencies": {
68
+ "zustand": "^5.0.11",
68
69
  "@microsoft/fetch-event-source": "^2.0.1",
69
70
  "@use-gesture/react": "^10.3.1",
70
71
  "bowser": "^2.14.1",