@djangocfg/ui-nextjs 2.1.65 → 2.1.67

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 (92) hide show
  1. package/package.json +13 -8
  2. package/src/blocks/SplitHero/SplitHeroMedia.tsx +2 -1
  3. package/src/stores/index.ts +8 -0
  4. package/src/stores/mediaCache.ts +464 -0
  5. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
  6. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
  7. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
  8. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
  9. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
  10. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
  11. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
  12. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
  13. package/src/tools/AudioPlayer/README.md +325 -0
  14. package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +200 -0
  15. package/src/tools/AudioPlayer/components/AudioPlayer.tsx +231 -0
  16. package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +99 -0
  17. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
  18. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  19. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  20. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  21. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  22. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  23. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  24. package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +280 -0
  25. package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +64 -0
  26. package/src/tools/AudioPlayer/components/index.ts +21 -0
  27. package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
  28. package/src/tools/AudioPlayer/context/index.ts +11 -0
  29. package/src/tools/AudioPlayer/context/selectors.ts +96 -0
  30. package/src/tools/AudioPlayer/effects/index.ts +412 -0
  31. package/src/tools/AudioPlayer/hooks/index.ts +29 -0
  32. package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
  33. package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +149 -0
  34. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
  35. package/src/tools/AudioPlayer/hooks/useVisualization.tsx +201 -0
  36. package/src/tools/AudioPlayer/index.ts +139 -0
  37. package/src/tools/AudioPlayer/types/audio.ts +107 -0
  38. package/src/tools/AudioPlayer/types/components.ts +98 -0
  39. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  40. package/src/tools/AudioPlayer/types/index.ts +35 -0
  41. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  42. package/src/tools/AudioPlayer/utils/index.ts +5 -0
  43. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  44. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  45. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  46. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  47. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  48. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  49. package/src/tools/ImageViewer/README.md +174 -0
  50. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  51. package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
  52. package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
  53. package/src/tools/ImageViewer/components/index.ts +7 -0
  54. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  55. package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
  56. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  57. package/src/tools/ImageViewer/index.ts +60 -0
  58. package/src/tools/ImageViewer/types.ts +75 -0
  59. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  60. package/src/tools/ImageViewer/utils/index.ts +16 -0
  61. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  62. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  63. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  64. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  65. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  66. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  67. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  68. package/src/tools/VideoPlayer/README.md +212 -187
  69. package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
  70. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +174 -0
  71. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
  72. package/src/tools/VideoPlayer/components/index.ts +14 -0
  73. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  74. package/src/tools/VideoPlayer/context/index.ts +8 -0
  75. package/src/tools/VideoPlayer/hooks/index.ts +9 -0
  76. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
  77. package/src/tools/VideoPlayer/index.ts +70 -9
  78. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +206 -0
  79. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +401 -0
  80. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +332 -0
  81. package/src/tools/VideoPlayer/providers/index.ts +8 -0
  82. package/src/tools/VideoPlayer/types/index.ts +38 -0
  83. package/src/tools/VideoPlayer/types/player.ts +116 -0
  84. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  85. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  86. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  87. package/src/tools/VideoPlayer/utils/index.ts +11 -0
  88. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  89. package/src/tools/index.ts +92 -4
  90. package/src/tools/VideoPlayer/NativePlayer.tsx +0 -141
  91. package/src/tools/VideoPlayer/VideoPlayer.tsx +0 -231
  92. package/src/tools/VideoPlayer/types.ts +0 -118
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-nextjs",
3
- "version": "2.1.65",
3
+ "version": "2.1.67",
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.65",
62
- "@djangocfg/ui-core": "^2.1.65",
61
+ "@djangocfg/api": "^2.1.67",
62
+ "@djangocfg/ui-core": "^2.1.67",
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",
@@ -78,16 +79,17 @@
78
79
  "@radix-ui/react-menubar": "^1.1.16",
79
80
  "@radix-ui/react-navigation-menu": "^1.2.14",
80
81
  "@radix-ui/react-slot": "^1.2.4",
81
- "input-otp": "1.4.2",
82
82
  "@rjsf/core": "^6.1.2",
83
83
  "@rjsf/utils": "^6.1.2",
84
84
  "@rjsf/validator-ajv8": "^6.1.2",
85
85
  "@vidstack/react": "next",
86
+ "@wavesurfer/react": "^1.0.12",
86
87
  "@web3icons/react": "^4.0.26",
87
88
  "chart.js": "^4.5.0",
88
89
  "class-variance-authority": "^0.7.1",
89
90
  "cytoscape": "^3.33.1",
90
91
  "cytoscape-cose-bilkent": "^4.1.0",
92
+ "input-otp": "1.4.2",
91
93
  "libphonenumber-js": "^1.12.24",
92
94
  "media-icons": "next",
93
95
  "mermaid": "^11.12.0",
@@ -100,17 +102,20 @@
100
102
  "react-lottie-player": "^2.1.0",
101
103
  "react-markdown": "10.1.0",
102
104
  "react-sticky-box": "^2.0.5",
105
+ "react-zoom-pan-pinch": "^3.7.0",
103
106
  "recharts": "2.15.4",
104
107
  "remark-gfm": "4.0.1",
105
108
  "sonner": "2.0.7",
106
- "vidstack": "next"
109
+ "vidstack": "next",
110
+ "wavesurfer.js": "^7.12.1"
107
111
  },
108
112
  "devDependencies": {
109
- "@djangocfg/typescript-config": "^2.1.65",
113
+ "@djangocfg/typescript-config": "^2.1.67",
110
114
  "@types/node": "^24.7.2",
111
115
  "eslint": "^9.37.0",
112
116
  "tailwindcss-animate": "1.0.7",
113
- "typescript": "^5.9.3"
117
+ "typescript": "^5.9.3",
118
+ "zustand": "^5.0.9"
114
119
  },
115
120
  "publishConfig": {
116
121
  "access": "public"
@@ -41,13 +41,14 @@ export const SplitHeroMedia: React.FC<SplitHeroMediaProps> = ({
41
41
  <div className={containerClass}>
42
42
  <VideoPlayer
43
43
  source={{
44
+ type: 'url',
44
45
  url: media.url,
45
46
  title: media.title,
46
47
  poster: media.poster,
47
48
  }}
48
49
  theme="modern"
49
50
  aspectRatio={16 / 9}
50
- autoplay={media.autoplay}
51
+ autoPlay={media.autoplay}
51
52
  muted={media.muted ?? media.autoplay}
52
53
  />
53
54
  </div>
@@ -0,0 +1,8 @@
1
+ export {
2
+ useMediaCacheStore,
3
+ useImageCache,
4
+ useAudioCache,
5
+ useVideoCache,
6
+ useBlobUrlCleanup,
7
+ generateContentKey,
8
+ } from './mediaCache';
@@ -0,0 +1,464 @@
1
+ 'use client';
2
+
3
+ import { create } from 'zustand';
4
+ import { persist, devtools } from 'zustand/middleware';
5
+
6
+ // Types
7
+ interface BlobUrlEntry {
8
+ url: string;
9
+ refCount: number;
10
+ createdAt: number;
11
+ }
12
+
13
+ interface ImageDimensions {
14
+ width: number;
15
+ height: number;
16
+ }
17
+
18
+ interface VideoMetadata {
19
+ duration: number;
20
+ width: number;
21
+ height: number;
22
+ codec?: string;
23
+ }
24
+
25
+ interface EffectConfig {
26
+ opacity: number;
27
+ scale: number;
28
+ blur: string;
29
+ }
30
+
31
+ // Stream URL TTL (5 minutes)
32
+ const STREAM_URL_TTL = 5 * 60 * 1000;
33
+
34
+ interface MediaCacheState {
35
+ // Blob URL cache (shared by Image, Audio, Video)
36
+ blobUrls: Map<string, BlobUrlEntry>;
37
+
38
+ // Image-specific
39
+ imageDimensions: Map<string, ImageDimensions>;
40
+
41
+ // Audio-specific
42
+ audioPlaybackPositions: Map<string, number>;
43
+ audioEffectConfigs: Map<string, EffectConfig>;
44
+
45
+ // Video-specific
46
+ videoStreamUrls: Map<string, { url: string; timestamp: number }>;
47
+ videoPosterUrls: Map<string, string>;
48
+ videoPlaybackPositions: Map<string, number>;
49
+ videoMetadata: Map<string, VideoMetadata>;
50
+ }
51
+
52
+ interface MediaCacheActions {
53
+ // Blob URL management (shared)
54
+ getOrCreateBlobUrl: (
55
+ key: string,
56
+ content: ArrayBuffer,
57
+ mimeType: string
58
+ ) => string;
59
+ releaseBlobUrl: (key: string) => void;
60
+ hasBlobUrl: (key: string) => boolean;
61
+
62
+ // Image actions
63
+ cacheDimensions: (src: string, dims: ImageDimensions) => void;
64
+ getDimensions: (src: string) => ImageDimensions | null;
65
+
66
+ // Audio actions
67
+ saveAudioPosition: (uri: string, time: number) => void;
68
+ getAudioPosition: (uri: string) => number | null;
69
+ getEffectConfig: (key: string) => EffectConfig | null;
70
+ cacheEffectConfig: (key: string, config: EffectConfig) => void;
71
+
72
+ // Video actions
73
+ getOrCreateStreamUrl: (
74
+ sessionId: string,
75
+ path: string,
76
+ generator: (sessionId: string, path: string) => string
77
+ ) => string;
78
+ getPosterUrl: (title: string) => string | null;
79
+ cachePosterUrl: (title: string, url: string) => void;
80
+ saveVideoPosition: (key: string, time: number) => void;
81
+ getVideoPosition: (key: string) => number | null;
82
+ cacheVideoMetadata: (url: string, meta: VideoMetadata) => void;
83
+ getVideoMetadata: (url: string) => VideoMetadata | null;
84
+ invalidateSession: (sessionId: string) => void;
85
+
86
+ // Global
87
+ clearCache: () => void;
88
+ getCacheStats: () => {
89
+ blobUrls: number;
90
+ dimensions: number;
91
+ audioPositions: number;
92
+ videoPositions: number;
93
+ };
94
+ }
95
+
96
+ type MediaCacheStore = MediaCacheState & MediaCacheActions;
97
+
98
+ // Initial state
99
+ const initialState: MediaCacheState = {
100
+ blobUrls: new Map(),
101
+ imageDimensions: new Map(),
102
+ audioPlaybackPositions: new Map(),
103
+ audioEffectConfigs: new Map(),
104
+ videoStreamUrls: new Map(),
105
+ videoPosterUrls: new Map(),
106
+ videoPlaybackPositions: new Map(),
107
+ videoMetadata: new Map(),
108
+ };
109
+
110
+ export const useMediaCacheStore = create<MediaCacheStore>()(
111
+ devtools(
112
+ persist(
113
+ (set, get) => ({
114
+ ...initialState,
115
+
116
+ // ========== Blob URL Management ==========
117
+
118
+ getOrCreateBlobUrl: (key, content, mimeType) => {
119
+ const existing = get().blobUrls.get(key);
120
+ if (existing) {
121
+ // Increment ref count
122
+ set(
123
+ (state) => ({
124
+ blobUrls: new Map(state.blobUrls).set(key, {
125
+ ...existing,
126
+ refCount: existing.refCount + 1,
127
+ }),
128
+ }),
129
+ false,
130
+ 'blobUrl/incrementRef'
131
+ );
132
+ return existing.url;
133
+ }
134
+
135
+ // Create new blob URL
136
+ const blob = new Blob([content], { type: mimeType });
137
+ const url = URL.createObjectURL(blob);
138
+ set(
139
+ (state) => ({
140
+ blobUrls: new Map(state.blobUrls).set(key, {
141
+ url,
142
+ refCount: 1,
143
+ createdAt: Date.now(),
144
+ }),
145
+ }),
146
+ false,
147
+ 'blobUrl/create'
148
+ );
149
+ return url;
150
+ },
151
+
152
+ releaseBlobUrl: (key) => {
153
+ const entry = get().blobUrls.get(key);
154
+ if (!entry) return;
155
+
156
+ if (entry.refCount <= 1) {
157
+ // Last reference - revoke and remove
158
+ URL.revokeObjectURL(entry.url);
159
+ set(
160
+ (state) => {
161
+ const newMap = new Map(state.blobUrls);
162
+ newMap.delete(key);
163
+ return { blobUrls: newMap };
164
+ },
165
+ false,
166
+ 'blobUrl/revoke'
167
+ );
168
+ } else {
169
+ // Decrement ref count
170
+ set(
171
+ (state) => ({
172
+ blobUrls: new Map(state.blobUrls).set(key, {
173
+ ...entry,
174
+ refCount: entry.refCount - 1,
175
+ }),
176
+ }),
177
+ false,
178
+ 'blobUrl/decrementRef'
179
+ );
180
+ }
181
+ },
182
+
183
+ hasBlobUrl: (key) => get().blobUrls.has(key),
184
+
185
+ // ========== Image Cache ==========
186
+
187
+ cacheDimensions: (src, dims) => {
188
+ set(
189
+ (state) => ({
190
+ imageDimensions: new Map(state.imageDimensions).set(src, dims),
191
+ }),
192
+ false,
193
+ 'image/cacheDimensions'
194
+ );
195
+ },
196
+
197
+ getDimensions: (src) => get().imageDimensions.get(src) ?? null,
198
+
199
+ // ========== Audio Cache ==========
200
+
201
+ saveAudioPosition: (uri, time) => {
202
+ set(
203
+ (state) => ({
204
+ audioPlaybackPositions: new Map(state.audioPlaybackPositions).set(
205
+ uri,
206
+ time
207
+ ),
208
+ }),
209
+ false,
210
+ 'audio/savePosition'
211
+ );
212
+ },
213
+
214
+ getAudioPosition: (uri) =>
215
+ get().audioPlaybackPositions.get(uri) ?? null,
216
+
217
+ getEffectConfig: (key) => get().audioEffectConfigs.get(key) ?? null,
218
+
219
+ cacheEffectConfig: (key, config) => {
220
+ set(
221
+ (state) => ({
222
+ audioEffectConfigs: new Map(state.audioEffectConfigs).set(
223
+ key,
224
+ config
225
+ ),
226
+ }),
227
+ false,
228
+ 'audio/cacheEffectConfig'
229
+ );
230
+ },
231
+
232
+ // ========== Video Cache ==========
233
+
234
+ getOrCreateStreamUrl: (sessionId, path, generator) => {
235
+ const key = `${sessionId}:${path}`;
236
+ const cached = get().videoStreamUrls.get(key);
237
+
238
+ // Return if fresh
239
+ if (cached && Date.now() - cached.timestamp < STREAM_URL_TTL) {
240
+ return cached.url;
241
+ }
242
+
243
+ // Generate and cache
244
+ const url = generator(sessionId, path);
245
+ set(
246
+ (state) => ({
247
+ videoStreamUrls: new Map(state.videoStreamUrls).set(key, {
248
+ url,
249
+ timestamp: Date.now(),
250
+ }),
251
+ }),
252
+ false,
253
+ 'video/cacheStreamUrl'
254
+ );
255
+ return url;
256
+ },
257
+
258
+ getPosterUrl: (title) => get().videoPosterUrls.get(title) ?? null,
259
+
260
+ cachePosterUrl: (title, url) => {
261
+ set(
262
+ (state) => ({
263
+ videoPosterUrls: new Map(state.videoPosterUrls).set(title, url),
264
+ }),
265
+ false,
266
+ 'video/cachePosterUrl'
267
+ );
268
+ },
269
+
270
+ saveVideoPosition: (key, time) => {
271
+ set(
272
+ (state) => ({
273
+ videoPlaybackPositions: new Map(state.videoPlaybackPositions).set(
274
+ key,
275
+ time
276
+ ),
277
+ }),
278
+ false,
279
+ 'video/savePosition'
280
+ );
281
+ },
282
+
283
+ getVideoPosition: (key) =>
284
+ get().videoPlaybackPositions.get(key) ?? null,
285
+
286
+ cacheVideoMetadata: (url, meta) => {
287
+ set(
288
+ (state) => ({
289
+ videoMetadata: new Map(state.videoMetadata).set(url, meta),
290
+ }),
291
+ false,
292
+ 'video/cacheMetadata'
293
+ );
294
+ },
295
+
296
+ getVideoMetadata: (url) => get().videoMetadata.get(url) ?? null,
297
+
298
+ invalidateSession: (sessionId) => {
299
+ set(
300
+ (state) => {
301
+ const newUrls = new Map(state.videoStreamUrls);
302
+ for (const [key] of newUrls) {
303
+ if (key.startsWith(`${sessionId}:`)) {
304
+ newUrls.delete(key);
305
+ }
306
+ }
307
+ return { videoStreamUrls: newUrls };
308
+ },
309
+ false,
310
+ 'video/invalidateSession'
311
+ );
312
+ },
313
+
314
+ // ========== Global ==========
315
+
316
+ clearCache: () => {
317
+ // Revoke all blob URLs before clearing
318
+ get().blobUrls.forEach(({ url }) => URL.revokeObjectURL(url));
319
+ set(initialState, false, 'clearCache');
320
+ },
321
+
322
+ getCacheStats: () => ({
323
+ blobUrls: get().blobUrls.size,
324
+ dimensions: get().imageDimensions.size,
325
+ audioPositions: get().audioPlaybackPositions.size,
326
+ videoPositions: get().videoPlaybackPositions.size,
327
+ }),
328
+ }),
329
+ {
330
+ name: 'media-cache-storage',
331
+ // Only persist playback positions and poster URLs
332
+ partialize: (state) => ({
333
+ audioPlaybackPositions: Array.from(
334
+ state.audioPlaybackPositions.entries()
335
+ ),
336
+ videoPlaybackPositions: Array.from(
337
+ state.videoPlaybackPositions.entries()
338
+ ),
339
+ videoPosterUrls: Array.from(state.videoPosterUrls.entries()),
340
+ }),
341
+ // Rehydrate Maps from arrays
342
+ onRehydrateStorage: () => (state) => {
343
+ if (state) {
344
+ // Type assertion for persisted data
345
+ const persistedAudio = state.audioPlaybackPositions as unknown;
346
+ const persistedVideo = state.videoPlaybackPositions as unknown;
347
+ const persistedPosters = state.videoPosterUrls as unknown;
348
+
349
+ state.audioPlaybackPositions = new Map(
350
+ Array.isArray(persistedAudio)
351
+ ? (persistedAudio as [string, number][])
352
+ : []
353
+ );
354
+ state.videoPlaybackPositions = new Map(
355
+ Array.isArray(persistedVideo)
356
+ ? (persistedVideo as [string, number][])
357
+ : []
358
+ );
359
+ state.videoPosterUrls = new Map(
360
+ Array.isArray(persistedPosters)
361
+ ? (persistedPosters as [string, string][])
362
+ : []
363
+ );
364
+ }
365
+ },
366
+ }
367
+ ),
368
+ { name: 'MediaCacheStore' }
369
+ )
370
+ );
371
+
372
+ // ========== Selective Hooks ==========
373
+
374
+ /**
375
+ * Hook for image-related cache operations
376
+ */
377
+ export const useImageCache = () =>
378
+ useMediaCacheStore((state) => ({
379
+ getOrCreateBlobUrl: state.getOrCreateBlobUrl,
380
+ releaseBlobUrl: state.releaseBlobUrl,
381
+ hasBlobUrl: state.hasBlobUrl,
382
+ cacheDimensions: state.cacheDimensions,
383
+ getDimensions: state.getDimensions,
384
+ }));
385
+
386
+ /**
387
+ * Hook for audio-related cache operations
388
+ */
389
+ export const useAudioCache = () =>
390
+ useMediaCacheStore((state) => ({
391
+ getOrCreateBlobUrl: state.getOrCreateBlobUrl,
392
+ releaseBlobUrl: state.releaseBlobUrl,
393
+ hasBlobUrl: state.hasBlobUrl,
394
+ saveAudioPosition: state.saveAudioPosition,
395
+ getAudioPosition: state.getAudioPosition,
396
+ getEffectConfig: state.getEffectConfig,
397
+ cacheEffectConfig: state.cacheEffectConfig,
398
+ }));
399
+
400
+ /**
401
+ * Hook for video-related cache operations
402
+ */
403
+ export const useVideoCache = () =>
404
+ useMediaCacheStore((state) => ({
405
+ getOrCreateBlobUrl: state.getOrCreateBlobUrl,
406
+ releaseBlobUrl: state.releaseBlobUrl,
407
+ hasBlobUrl: state.hasBlobUrl,
408
+ getOrCreateStreamUrl: state.getOrCreateStreamUrl,
409
+ getPosterUrl: state.getPosterUrl,
410
+ cachePosterUrl: state.cachePosterUrl,
411
+ saveVideoPosition: state.saveVideoPosition,
412
+ getVideoPosition: state.getVideoPosition,
413
+ cacheVideoMetadata: state.cacheVideoMetadata,
414
+ getVideoMetadata: state.getVideoMetadata,
415
+ invalidateSession: state.invalidateSession,
416
+ }));
417
+
418
+ /**
419
+ * Hook for blob URL cleanup on unmount
420
+ */
421
+ export function useBlobUrlCleanup(key: string | null) {
422
+ const releaseBlobUrl = useMediaCacheStore((s) => s.releaseBlobUrl);
423
+
424
+ // Using inline effect to avoid importing useEffect in store
425
+ if (typeof window !== 'undefined') {
426
+ // eslint-disable-next-line react-hooks/rules-of-hooks
427
+ const { useEffect } = require('react');
428
+ // eslint-disable-next-line react-hooks/rules-of-hooks
429
+ useEffect(() => {
430
+ return () => {
431
+ if (key) {
432
+ releaseBlobUrl(key);
433
+ }
434
+ };
435
+ }, [key, releaseBlobUrl]);
436
+ }
437
+ }
438
+
439
+ // ========== Utilities ==========
440
+
441
+ /**
442
+ * Generate a cache key from ArrayBuffer content
443
+ * Uses first and last 1KB + length for fast hashing
444
+ */
445
+ export function generateContentKey(content: ArrayBuffer): string {
446
+ const arr = new Uint8Array(content);
447
+ const len = arr.length;
448
+
449
+ // Take first 1KB
450
+ const start = arr.slice(0, Math.min(1024, len));
451
+ // Take last 1KB
452
+ const end = arr.slice(Math.max(0, len - 1024));
453
+
454
+ // Simple hash from bytes
455
+ let hash = len;
456
+ for (let i = 0; i < start.length; i++) {
457
+ hash = ((hash << 5) - hash + start[i]) | 0;
458
+ }
459
+ for (let i = 0; i < end.length; i++) {
460
+ hash = ((hash << 5) - hash + end[i]) | 0;
461
+ }
462
+
463
+ return `blob-${len}-${hash.toString(16)}`;
464
+ }
@@ -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