@djangocfg/ui-nextjs 2.1.66 → 2.1.68

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.
Files changed (90) hide show
  1. package/package.json +8 -6
  2. package/src/stores/index.ts +8 -0
  3. package/src/stores/mediaCache.ts +474 -0
  4. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
  5. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
  6. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
  7. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
  8. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
  9. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
  10. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
  11. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
  12. package/src/tools/AudioPlayer/README.md +35 -11
  13. package/src/tools/AudioPlayer/{AudioEqualizer.tsx → components/AudioEqualizer.tsx} +29 -64
  14. package/src/tools/AudioPlayer/{AudioPlayer.tsx → components/AudioPlayer.tsx} +22 -14
  15. package/src/tools/AudioPlayer/{AudioShortcutsPopover.tsx → components/AudioShortcutsPopover.tsx} +6 -2
  16. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
  17. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  18. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  19. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  20. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  21. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  22. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  23. package/src/tools/AudioPlayer/{SimpleAudioPlayer.tsx → components/SimpleAudioPlayer.tsx} +12 -7
  24. package/src/tools/AudioPlayer/{VisualizationToggle.tsx → components/VisualizationToggle.tsx} +2 -6
  25. package/src/tools/AudioPlayer/components/index.ts +21 -0
  26. package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
  27. package/src/tools/AudioPlayer/context/index.ts +11 -0
  28. package/src/tools/AudioPlayer/context/selectors.ts +96 -0
  29. package/src/tools/AudioPlayer/hooks/index.ts +29 -0
  30. package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
  31. package/src/tools/AudioPlayer/{useAudioHotkeys.ts → hooks/useAudioHotkeys.ts} +11 -4
  32. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
  33. package/src/tools/AudioPlayer/{useAudioVisualization.tsx → hooks/useVisualization.tsx} +11 -5
  34. package/src/tools/AudioPlayer/index.ts +104 -49
  35. package/src/tools/AudioPlayer/types/audio.ts +107 -0
  36. package/src/tools/AudioPlayer/{types.ts → types/components.ts} +20 -84
  37. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  38. package/src/tools/AudioPlayer/types/index.ts +35 -0
  39. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  40. package/src/tools/AudioPlayer/utils/index.ts +5 -0
  41. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  42. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  43. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  44. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  45. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  46. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  47. package/src/tools/ImageViewer/README.md +16 -3
  48. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  49. package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
  50. package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
  51. package/src/tools/ImageViewer/components/index.ts +7 -0
  52. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  53. package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
  54. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  55. package/src/tools/ImageViewer/index.ts +47 -3
  56. package/src/tools/ImageViewer/types.ts +75 -0
  57. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  58. package/src/tools/ImageViewer/utils/index.ts +16 -0
  59. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  60. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  61. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  62. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  63. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  64. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  65. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  66. package/src/tools/VideoPlayer/README.md +26 -10
  67. package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
  68. package/src/tools/VideoPlayer/{VideoErrorFallback.tsx → components/VideoErrorFallback.tsx} +2 -2
  69. package/src/tools/VideoPlayer/{VideoPlayer.tsx → components/VideoPlayer.tsx} +4 -5
  70. package/src/tools/VideoPlayer/components/index.ts +14 -0
  71. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  72. package/src/tools/VideoPlayer/context/index.ts +8 -0
  73. package/src/tools/VideoPlayer/hooks/index.ts +9 -0
  74. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
  75. package/src/tools/VideoPlayer/index.ts +29 -20
  76. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +118 -28
  77. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +89 -11
  78. package/src/tools/VideoPlayer/types/index.ts +38 -0
  79. package/src/tools/VideoPlayer/types/player.ts +116 -0
  80. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  81. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  82. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  83. package/src/tools/VideoPlayer/utils/index.ts +11 -0
  84. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  85. package/src/tools/index.ts +10 -0
  86. package/src/tools/AudioPlayer/AudioReactiveCover.tsx +0 -389
  87. package/src/tools/AudioPlayer/context.tsx +0 -426
  88. package/src/tools/ImageViewer/ImageViewer.tsx +0 -416
  89. package/src/tools/VideoPlayer/VideoPlayerContext.tsx +0 -125
  90. package/src/tools/VideoPlayer/types.ts +0 -367
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-nextjs",
3
- "version": "2.1.66",
3
+ "version": "2.1.68",
4
4
  "description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -58,8 +58,8 @@
58
58
  "check": "tsc --noEmit"
59
59
  },
60
60
  "peerDependencies": {
61
- "@djangocfg/api": "^2.1.66",
62
- "@djangocfg/ui-core": "^2.1.66",
61
+ "@djangocfg/api": "^2.1.68",
62
+ "@djangocfg/ui-core": "^2.1.68",
63
63
  "@types/react": "^19.1.0",
64
64
  "@types/react-dom": "^19.1.0",
65
65
  "consola": "^3.4.2",
@@ -70,7 +70,8 @@
70
70
  "react-dom": "^19.1.0",
71
71
  "react-hook-form": "7.65.0",
72
72
  "tailwindcss": "^4.1.14",
73
- "zod": "^4.1.13"
73
+ "zod": "^4.1.13",
74
+ "zustand": "^5.0.9"
74
75
  },
75
76
  "dependencies": {
76
77
  "@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -109,11 +110,12 @@
109
110
  "wavesurfer.js": "^7.12.1"
110
111
  },
111
112
  "devDependencies": {
112
- "@djangocfg/typescript-config": "^2.1.66",
113
+ "@djangocfg/typescript-config": "^2.1.68",
113
114
  "@types/node": "^24.7.2",
114
115
  "eslint": "^9.37.0",
115
116
  "tailwindcss-animate": "1.0.7",
116
- "typescript": "^5.9.3"
117
+ "typescript": "^5.9.3",
118
+ "zustand": "^5.0.9"
117
119
  },
118
120
  "publishConfig": {
119
121
  "access": "public"
@@ -0,0 +1,8 @@
1
+ export {
2
+ useMediaCacheStore,
3
+ useImageCache,
4
+ useAudioCache,
5
+ useVideoCache,
6
+ useBlobUrlCleanup,
7
+ generateContentKey,
8
+ } from './mediaCache';
@@ -0,0 +1,474 @@
1
+ 'use client';
2
+
3
+ import { create } from 'zustand';
4
+ import { persist, devtools } from 'zustand/middleware';
5
+ import { useShallow } from 'zustand/react/shallow';
6
+
7
+ // Types
8
+ interface BlobUrlEntry {
9
+ url: string;
10
+ refCount: number;
11
+ createdAt: number;
12
+ }
13
+
14
+ interface ImageDimensions {
15
+ width: number;
16
+ height: number;
17
+ }
18
+
19
+ interface VideoMetadata {
20
+ duration: number;
21
+ width: number;
22
+ height: number;
23
+ codec?: string;
24
+ }
25
+
26
+ interface EffectConfig {
27
+ opacity: number;
28
+ scale: number;
29
+ blur: string;
30
+ }
31
+
32
+ // Stream URL TTL (5 minutes)
33
+ const STREAM_URL_TTL = 5 * 60 * 1000;
34
+
35
+ interface MediaCacheState {
36
+ // Blob URL cache (shared by Image, Audio, Video)
37
+ blobUrls: Map<string, BlobUrlEntry>;
38
+
39
+ // Image-specific
40
+ imageDimensions: Map<string, ImageDimensions>;
41
+
42
+ // Audio-specific
43
+ audioPlaybackPositions: Map<string, number>;
44
+ audioEffectConfigs: Map<string, EffectConfig>;
45
+
46
+ // Video-specific
47
+ videoStreamUrls: Map<string, { url: string; timestamp: number }>;
48
+ videoPosterUrls: Map<string, string>;
49
+ videoPlaybackPositions: Map<string, number>;
50
+ videoMetadata: Map<string, VideoMetadata>;
51
+ }
52
+
53
+ interface MediaCacheActions {
54
+ // Blob URL management (shared)
55
+ getOrCreateBlobUrl: (
56
+ key: string,
57
+ content: ArrayBuffer,
58
+ mimeType: string
59
+ ) => string;
60
+ releaseBlobUrl: (key: string) => void;
61
+ hasBlobUrl: (key: string) => boolean;
62
+
63
+ // Image actions
64
+ cacheDimensions: (src: string, dims: ImageDimensions) => void;
65
+ getDimensions: (src: string) => ImageDimensions | null;
66
+
67
+ // Audio actions
68
+ saveAudioPosition: (uri: string, time: number) => void;
69
+ getAudioPosition: (uri: string) => number | null;
70
+ getEffectConfig: (key: string) => EffectConfig | null;
71
+ cacheEffectConfig: (key: string, config: EffectConfig) => void;
72
+
73
+ // Video actions
74
+ getOrCreateStreamUrl: (
75
+ sessionId: string,
76
+ path: string,
77
+ generator: (sessionId: string, path: string) => string
78
+ ) => string;
79
+ getPosterUrl: (title: string) => string | null;
80
+ cachePosterUrl: (title: string, url: string) => void;
81
+ saveVideoPosition: (key: string, time: number) => void;
82
+ getVideoPosition: (key: string) => number | null;
83
+ cacheVideoMetadata: (url: string, meta: VideoMetadata) => void;
84
+ getVideoMetadata: (url: string) => VideoMetadata | null;
85
+ invalidateSession: (sessionId: string) => void;
86
+
87
+ // Global
88
+ clearCache: () => void;
89
+ getCacheStats: () => {
90
+ blobUrls: number;
91
+ dimensions: number;
92
+ audioPositions: number;
93
+ videoPositions: number;
94
+ };
95
+ }
96
+
97
+ type MediaCacheStore = MediaCacheState & MediaCacheActions;
98
+
99
+ // Initial state
100
+ const initialState: MediaCacheState = {
101
+ blobUrls: new Map(),
102
+ imageDimensions: new Map(),
103
+ audioPlaybackPositions: new Map(),
104
+ audioEffectConfigs: new Map(),
105
+ videoStreamUrls: new Map(),
106
+ videoPosterUrls: new Map(),
107
+ videoPlaybackPositions: new Map(),
108
+ videoMetadata: new Map(),
109
+ };
110
+
111
+ export const useMediaCacheStore = create<MediaCacheStore>()(
112
+ devtools(
113
+ persist(
114
+ (set, get) => ({
115
+ ...initialState,
116
+
117
+ // ========== Blob URL Management ==========
118
+
119
+ getOrCreateBlobUrl: (key, content, mimeType) => {
120
+ const existing = get().blobUrls.get(key);
121
+ if (existing) {
122
+ // Increment ref count
123
+ set(
124
+ (state) => ({
125
+ blobUrls: new Map(state.blobUrls).set(key, {
126
+ ...existing,
127
+ refCount: existing.refCount + 1,
128
+ }),
129
+ }),
130
+ false,
131
+ 'blobUrl/incrementRef'
132
+ );
133
+ return existing.url;
134
+ }
135
+
136
+ // Create new blob URL
137
+ const blob = new Blob([content], { type: mimeType });
138
+ const url = URL.createObjectURL(blob);
139
+ set(
140
+ (state) => ({
141
+ blobUrls: new Map(state.blobUrls).set(key, {
142
+ url,
143
+ refCount: 1,
144
+ createdAt: Date.now(),
145
+ }),
146
+ }),
147
+ false,
148
+ 'blobUrl/create'
149
+ );
150
+ return url;
151
+ },
152
+
153
+ releaseBlobUrl: (key) => {
154
+ const entry = get().blobUrls.get(key);
155
+ if (!entry) return;
156
+
157
+ if (entry.refCount <= 1) {
158
+ // Last reference - revoke and remove
159
+ URL.revokeObjectURL(entry.url);
160
+ set(
161
+ (state) => {
162
+ const newMap = new Map(state.blobUrls);
163
+ newMap.delete(key);
164
+ return { blobUrls: newMap };
165
+ },
166
+ false,
167
+ 'blobUrl/revoke'
168
+ );
169
+ } else {
170
+ // Decrement ref count
171
+ set(
172
+ (state) => ({
173
+ blobUrls: new Map(state.blobUrls).set(key, {
174
+ ...entry,
175
+ refCount: entry.refCount - 1,
176
+ }),
177
+ }),
178
+ false,
179
+ 'blobUrl/decrementRef'
180
+ );
181
+ }
182
+ },
183
+
184
+ hasBlobUrl: (key) => get().blobUrls.has(key),
185
+
186
+ // ========== Image Cache ==========
187
+
188
+ cacheDimensions: (src, dims) => {
189
+ set(
190
+ (state) => ({
191
+ imageDimensions: new Map(state.imageDimensions).set(src, dims),
192
+ }),
193
+ false,
194
+ 'image/cacheDimensions'
195
+ );
196
+ },
197
+
198
+ getDimensions: (src) => get().imageDimensions.get(src) ?? null,
199
+
200
+ // ========== Audio Cache ==========
201
+
202
+ saveAudioPosition: (uri, time) => {
203
+ set(
204
+ (state) => ({
205
+ audioPlaybackPositions: new Map(state.audioPlaybackPositions).set(
206
+ uri,
207
+ time
208
+ ),
209
+ }),
210
+ false,
211
+ 'audio/savePosition'
212
+ );
213
+ },
214
+
215
+ getAudioPosition: (uri) =>
216
+ get().audioPlaybackPositions.get(uri) ?? null,
217
+
218
+ getEffectConfig: (key) => get().audioEffectConfigs.get(key) ?? null,
219
+
220
+ cacheEffectConfig: (key, config) => {
221
+ set(
222
+ (state) => ({
223
+ audioEffectConfigs: new Map(state.audioEffectConfigs).set(
224
+ key,
225
+ config
226
+ ),
227
+ }),
228
+ false,
229
+ 'audio/cacheEffectConfig'
230
+ );
231
+ },
232
+
233
+ // ========== Video Cache ==========
234
+
235
+ getOrCreateStreamUrl: (sessionId, path, generator) => {
236
+ const key = `${sessionId}:${path}`;
237
+ const cached = get().videoStreamUrls.get(key);
238
+
239
+ // Return if fresh
240
+ if (cached && Date.now() - cached.timestamp < STREAM_URL_TTL) {
241
+ return cached.url;
242
+ }
243
+
244
+ // Generate and cache
245
+ const url = generator(sessionId, path);
246
+ set(
247
+ (state) => ({
248
+ videoStreamUrls: new Map(state.videoStreamUrls).set(key, {
249
+ url,
250
+ timestamp: Date.now(),
251
+ }),
252
+ }),
253
+ false,
254
+ 'video/cacheStreamUrl'
255
+ );
256
+ return url;
257
+ },
258
+
259
+ getPosterUrl: (title) => get().videoPosterUrls.get(title) ?? null,
260
+
261
+ cachePosterUrl: (title, url) => {
262
+ set(
263
+ (state) => ({
264
+ videoPosterUrls: new Map(state.videoPosterUrls).set(title, url),
265
+ }),
266
+ false,
267
+ 'video/cachePosterUrl'
268
+ );
269
+ },
270
+
271
+ saveVideoPosition: (key, time) => {
272
+ set(
273
+ (state) => ({
274
+ videoPlaybackPositions: new Map(state.videoPlaybackPositions).set(
275
+ key,
276
+ time
277
+ ),
278
+ }),
279
+ false,
280
+ 'video/savePosition'
281
+ );
282
+ },
283
+
284
+ getVideoPosition: (key) =>
285
+ get().videoPlaybackPositions.get(key) ?? null,
286
+
287
+ cacheVideoMetadata: (url, meta) => {
288
+ set(
289
+ (state) => ({
290
+ videoMetadata: new Map(state.videoMetadata).set(url, meta),
291
+ }),
292
+ false,
293
+ 'video/cacheMetadata'
294
+ );
295
+ },
296
+
297
+ getVideoMetadata: (url) => get().videoMetadata.get(url) ?? null,
298
+
299
+ invalidateSession: (sessionId) => {
300
+ set(
301
+ (state) => {
302
+ const newUrls = new Map(state.videoStreamUrls);
303
+ for (const [key] of newUrls) {
304
+ if (key.startsWith(`${sessionId}:`)) {
305
+ newUrls.delete(key);
306
+ }
307
+ }
308
+ return { videoStreamUrls: newUrls };
309
+ },
310
+ false,
311
+ 'video/invalidateSession'
312
+ );
313
+ },
314
+
315
+ // ========== Global ==========
316
+
317
+ clearCache: () => {
318
+ // Revoke all blob URLs before clearing
319
+ get().blobUrls.forEach(({ url }) => URL.revokeObjectURL(url));
320
+ set(initialState, false, 'clearCache');
321
+ },
322
+
323
+ getCacheStats: () => ({
324
+ blobUrls: get().blobUrls.size,
325
+ dimensions: get().imageDimensions.size,
326
+ audioPositions: get().audioPlaybackPositions.size,
327
+ videoPositions: get().videoPlaybackPositions.size,
328
+ }),
329
+ }),
330
+ {
331
+ name: 'media-cache-storage',
332
+ // Only persist playback positions and poster URLs
333
+ partialize: (state) => ({
334
+ audioPlaybackPositions: Array.from(
335
+ state.audioPlaybackPositions.entries()
336
+ ),
337
+ videoPlaybackPositions: Array.from(
338
+ state.videoPlaybackPositions.entries()
339
+ ),
340
+ videoPosterUrls: Array.from(state.videoPosterUrls.entries()),
341
+ }),
342
+ // Rehydrate Maps from arrays
343
+ onRehydrateStorage: () => (state) => {
344
+ if (state) {
345
+ // Type assertion for persisted data
346
+ const persistedAudio = state.audioPlaybackPositions as unknown;
347
+ const persistedVideo = state.videoPlaybackPositions as unknown;
348
+ const persistedPosters = state.videoPosterUrls as unknown;
349
+
350
+ state.audioPlaybackPositions = new Map(
351
+ Array.isArray(persistedAudio)
352
+ ? (persistedAudio as [string, number][])
353
+ : []
354
+ );
355
+ state.videoPlaybackPositions = new Map(
356
+ Array.isArray(persistedVideo)
357
+ ? (persistedVideo as [string, number][])
358
+ : []
359
+ );
360
+ state.videoPosterUrls = new Map(
361
+ Array.isArray(persistedPosters)
362
+ ? (persistedPosters as [string, string][])
363
+ : []
364
+ );
365
+ }
366
+ },
367
+ }
368
+ ),
369
+ { name: 'MediaCacheStore' }
370
+ )
371
+ );
372
+
373
+ // ========== Selective Hooks ==========
374
+
375
+ /**
376
+ * Hook for image-related cache operations
377
+ * Uses useShallow to prevent infinite re-renders
378
+ */
379
+ export const useImageCache = () =>
380
+ useMediaCacheStore(
381
+ useShallow((state) => ({
382
+ getOrCreateBlobUrl: state.getOrCreateBlobUrl,
383
+ releaseBlobUrl: state.releaseBlobUrl,
384
+ hasBlobUrl: state.hasBlobUrl,
385
+ cacheDimensions: state.cacheDimensions,
386
+ getDimensions: state.getDimensions,
387
+ }))
388
+ );
389
+
390
+ /**
391
+ * Hook for audio-related cache operations
392
+ * Uses useShallow to prevent infinite re-renders
393
+ */
394
+ export const useAudioCache = () =>
395
+ useMediaCacheStore(
396
+ useShallow((state) => ({
397
+ getOrCreateBlobUrl: state.getOrCreateBlobUrl,
398
+ releaseBlobUrl: state.releaseBlobUrl,
399
+ hasBlobUrl: state.hasBlobUrl,
400
+ saveAudioPosition: state.saveAudioPosition,
401
+ getAudioPosition: state.getAudioPosition,
402
+ getEffectConfig: state.getEffectConfig,
403
+ cacheEffectConfig: state.cacheEffectConfig,
404
+ }))
405
+ );
406
+
407
+ /**
408
+ * Hook for video-related cache operations
409
+ * Uses useShallow to prevent infinite re-renders
410
+ */
411
+ export const useVideoCache = () =>
412
+ useMediaCacheStore(
413
+ useShallow((state) => ({
414
+ getOrCreateBlobUrl: state.getOrCreateBlobUrl,
415
+ releaseBlobUrl: state.releaseBlobUrl,
416
+ hasBlobUrl: state.hasBlobUrl,
417
+ getOrCreateStreamUrl: state.getOrCreateStreamUrl,
418
+ getPosterUrl: state.getPosterUrl,
419
+ cachePosterUrl: state.cachePosterUrl,
420
+ saveVideoPosition: state.saveVideoPosition,
421
+ getVideoPosition: state.getVideoPosition,
422
+ cacheVideoMetadata: state.cacheVideoMetadata,
423
+ getVideoMetadata: state.getVideoMetadata,
424
+ invalidateSession: state.invalidateSession,
425
+ }))
426
+ );
427
+
428
+ /**
429
+ * Hook for blob URL cleanup on unmount
430
+ */
431
+ export function useBlobUrlCleanup(key: string | null) {
432
+ const releaseBlobUrl = useMediaCacheStore((s) => s.releaseBlobUrl);
433
+
434
+ // Using inline effect to avoid importing useEffect in store
435
+ if (typeof window !== 'undefined') {
436
+ // eslint-disable-next-line react-hooks/rules-of-hooks
437
+ const { useEffect } = require('react');
438
+ // eslint-disable-next-line react-hooks/rules-of-hooks
439
+ useEffect(() => {
440
+ return () => {
441
+ if (key) {
442
+ releaseBlobUrl(key);
443
+ }
444
+ };
445
+ }, [key, releaseBlobUrl]);
446
+ }
447
+ }
448
+
449
+ // ========== Utilities ==========
450
+
451
+ /**
452
+ * Generate a cache key from ArrayBuffer content
453
+ * Uses first and last 1KB + length for fast hashing
454
+ */
455
+ export function generateContentKey(content: ArrayBuffer): string {
456
+ const arr = new Uint8Array(content);
457
+ const len = arr.length;
458
+
459
+ // Take first 1KB
460
+ const start = arr.slice(0, Math.min(1024, len));
461
+ // Take last 1KB
462
+ const end = arr.slice(Math.max(0, len - 1024));
463
+
464
+ // Simple hash from bytes
465
+ let hash = len;
466
+ for (let i = 0; i < start.length; i++) {
467
+ hash = ((hash << 5) - hash + start[i]) | 0;
468
+ }
469
+ for (let i = 0; i < end.length; i++) {
470
+ hash = ((hash << 5) - hash + end[i]) | 0;
471
+ }
472
+
473
+ return `blob-${len}-${hash.toString(16)}`;
474
+ }
@@ -0,0 +1,148 @@
1
+ # AudioPlayer Refactoring Plan
2
+
3
+ ## Goal
4
+
5
+ Decompose the AudioPlayer module into a clean, maintainable folder structure with proper separation of concerns.
6
+
7
+ ## Current Issues
8
+
9
+ 1. **`context.tsx` is too large (574 lines)** - contains 4 different hooks and provider
10
+ 2. **`types.ts` is monolithic (185 lines)** - mixes audio, component, and effect types
11
+ 3. **`effects/index.ts` is overloaded (413 lines)** - types, constants, and calculations together
12
+ 4. **`AudioReactiveCover.tsx` has inline effect components** - harder to maintain
13
+
14
+ ## Target Structure
15
+
16
+ ```
17
+ AudioPlayer/
18
+ ├── index.ts # Public API (re-exports)
19
+
20
+ ├── types/
21
+ │ ├── index.ts # Re-exports all types
22
+ │ ├── audio.ts # Core audio types
23
+ │ ├── components.ts # Component props
24
+ │ └── effects.ts # Effect-related types
25
+
26
+ ├── hooks/
27
+ │ ├── index.ts # Re-exports all hooks
28
+ │ ├── useSharedWebAudio.ts # Web Audio API management
29
+ │ ├── useAudioAnalysis.ts # Frequency analysis
30
+ │ ├── useAudioHotkeys.ts # Keyboard shortcuts (existing)
31
+ │ └── useVisualization.tsx # Settings + Provider (renamed)
32
+
33
+ ├── context/
34
+ │ ├── index.ts # Re-exports
35
+ │ ├── AudioProvider.tsx # Provider component only
36
+ │ └── selectors.ts # useAudio, useAudioControls, useAudioState, useAudioElement
37
+
38
+ ├── components/
39
+ │ ├── index.ts # Re-exports
40
+ │ ├── AudioPlayer.tsx # Main player UI
41
+ │ ├── AudioEqualizer.tsx # Equalizer visualization
42
+ │ ├── SimpleAudioPlayer.tsx # Easy-to-use wrapper
43
+ │ ├── ShortcutsPopover.tsx # Keyboard shortcuts popover
44
+ │ ├── VisualizationToggle.tsx # Effect toggle button
45
+ │ └── ReactiveCover/ # Cover with effects
46
+ │ ├── index.tsx # Main component
47
+ │ ├── GlowEffect.tsx # Glow effect
48
+ │ ├── OrbsEffect.tsx # Orbs effect
49
+ │ ├── SpotlightEffect.tsx # Spotlight effect
50
+ │ └── MeshEffect.tsx # Mesh gradient effect
51
+
52
+ ├── effects/
53
+ │ ├── index.ts # Re-exports
54
+ │ ├── constants.ts # INTENSITY_CONFIG, COLOR_SCHEMES
55
+ │ ├── calculations.ts # calculateGlowLayers, calculateOrbs, etc.
56
+ │ └── animations.ts # EFFECT_ANIMATIONS CSS string
57
+
58
+ └── utils/
59
+ ├── index.ts # Re-exports
60
+ └── formatTime.ts # Time formatting helper
61
+ ```
62
+
63
+ ## Migration Steps
64
+
65
+ ### Phase 1: Create folder structure
66
+ - [ ] Create `types/`, `hooks/`, `context/`, `components/`, `effects/`, `utils/` folders
67
+
68
+ ### Phase 2: Split types
69
+ - [ ] Create `types/audio.ts` - AudioSource, PlaybackStatus, SharedWebAudioContext, AudioContextState
70
+ - [ ] Create `types/components.ts` - AudioPlayerProps, AudioEqualizerProps, etc.
71
+ - [ ] Create `types/effects.ts` - EffectVariant, AudioLevels, EffectConfig, etc.
72
+ - [ ] Create `types/index.ts` - re-export all
73
+
74
+ ### Phase 3: Extract hooks from context.tsx
75
+ - [ ] Create `hooks/useSharedWebAudio.ts` - extract from context.tsx
76
+ - [ ] Create `hooks/useAudioAnalysis.ts` - extract from context.tsx
77
+ - [ ] Move `useAudioHotkeys.ts` to `hooks/`
78
+ - [ ] Move `useAudioVisualization.tsx` to `hooks/useVisualization.tsx`
79
+ - [ ] Create `hooks/index.ts`
80
+
81
+ ### Phase 4: Refactor context
82
+ - [ ] Create `context/AudioProvider.tsx` - provider only
83
+ - [ ] Create `context/selectors.ts` - useAudio, useAudioControls, useAudioState, useAudioElement
84
+ - [ ] Create `context/index.ts`
85
+
86
+ ### Phase 5: Reorganize components
87
+ - [ ] Move `AudioPlayer.tsx` to `components/`
88
+ - [ ] Move `AudioEqualizer.tsx` to `components/`
89
+ - [ ] Move `SimpleAudioPlayer.tsx` to `components/`
90
+ - [ ] Rename and move `AudioShortcutsPopover.tsx` to `components/ShortcutsPopover.tsx`
91
+ - [ ] Move `VisualizationToggle.tsx` to `components/`
92
+ - [ ] Split `AudioReactiveCover.tsx` into `components/ReactiveCover/`
93
+
94
+ ### Phase 6: Refactor effects
95
+ - [ ] Create `effects/constants.ts` - INTENSITY_CONFIG, COLOR_SCHEMES
96
+ - [ ] Create `effects/calculations.ts` - all calculate* functions
97
+ - [ ] Create `effects/animations.ts` - EFFECT_ANIMATIONS
98
+ - [ ] Update `effects/index.ts`
99
+
100
+ ### Phase 7: Create utils
101
+ - [ ] Create `utils/formatTime.ts` - extract from AudioPlayer.tsx
102
+ - [ ] Create `utils/index.ts`
103
+
104
+ ### Phase 8: Update main index.ts
105
+ - [ ] Update `index.ts` to re-export from new locations
106
+ - [ ] Ensure backward compatibility of public API
107
+
108
+ ### Phase 9: Cleanup
109
+ - [ ] Delete old files
110
+ - [ ] Run type check
111
+ - [ ] Test imports
112
+
113
+ ## File Size Targets
114
+
115
+ | File | Current | Target |
116
+ |------|---------|--------|
117
+ | context.tsx | 574 lines | Split into 4 files (~100-150 each) |
118
+ | types.ts | 185 lines | Split into 3 files (~60 each) |
119
+ | effects/index.ts | 413 lines | Split into 3 files (~100-150 each) |
120
+ | AudioReactiveCover.tsx | 390 lines | Split into 5 files (~80 each) |
121
+
122
+ ## Public API (must remain unchanged)
123
+
124
+ ```typescript
125
+ // Components
126
+ export { SimpleAudioPlayer } from './components';
127
+ export { AudioPlayer } from './components';
128
+ export { AudioEqualizer } from './components';
129
+ export { AudioReactiveCover } from './components';
130
+ export { AudioShortcutsPopover } from './components'; // alias for backward compat
131
+
132
+ // Context & Hooks
133
+ export { AudioProvider, useAudio, useAudioControls, useAudioState, useAudioElement } from './context';
134
+ export { useAudioHotkeys, AUDIO_SHORTCUTS } from './hooks';
135
+ export { useAudioVisualization, VisualizationProvider } from './hooks'; // alias
136
+
137
+ // Effects utilities
138
+ export { INTENSITY_CONFIG, COLOR_SCHEMES, ... } from './effects';
139
+
140
+ // Types (all existing types)
141
+ export type { AudioSource, PlaybackStatus, ... } from './types';
142
+ ```
143
+
144
+ ## Notes
145
+
146
+ - Keep backward compatibility for all public exports
147
+ - Internal imports will change but external API stays the same
148
+ - Run `pnpm check` after each phase to catch issues early