@djangocfg/ui-tools 2.1.91

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 (174) hide show
  1. package/dist/LottiePlayer.client-LBEC2JKY.mjs +161 -0
  2. package/dist/LottiePlayer.client-LBEC2JKY.mjs.map +1 -0
  3. package/dist/LottiePlayer.client-WFMG2OOW.cjs +168 -0
  4. package/dist/LottiePlayer.client-WFMG2OOW.cjs.map +1 -0
  5. package/dist/Mermaid.client-4TU2TSH3.mjs +477 -0
  6. package/dist/Mermaid.client-4TU2TSH3.mjs.map +1 -0
  7. package/dist/Mermaid.client-SBYY364Q.cjs +483 -0
  8. package/dist/Mermaid.client-SBYY364Q.cjs.map +1 -0
  9. package/dist/PlaygroundLayout-3YVSAEAF.cjs +1003 -0
  10. package/dist/PlaygroundLayout-3YVSAEAF.cjs.map +1 -0
  11. package/dist/PlaygroundLayout-4DYBORAS.mjs +996 -0
  12. package/dist/PlaygroundLayout-4DYBORAS.mjs.map +1 -0
  13. package/dist/PrettyCode.client-LCBPPTIX.mjs +152 -0
  14. package/dist/PrettyCode.client-LCBPPTIX.mjs.map +1 -0
  15. package/dist/PrettyCode.client-PNPLXRH6.cjs +154 -0
  16. package/dist/PrettyCode.client-PNPLXRH6.cjs.map +1 -0
  17. package/dist/chunk-37ZI6VD4.mjs +12 -0
  18. package/dist/chunk-37ZI6VD4.mjs.map +1 -0
  19. package/dist/chunk-3HK2OE62.cjs +81 -0
  20. package/dist/chunk-3HK2OE62.cjs.map +1 -0
  21. package/dist/chunk-7DGDQVQW.cjs +591 -0
  22. package/dist/chunk-7DGDQVQW.cjs.map +1 -0
  23. package/dist/chunk-M6P2FU7L.mjs +572 -0
  24. package/dist/chunk-M6P2FU7L.mjs.map +1 -0
  25. package/dist/chunk-UQ3XI5MY.cjs +15 -0
  26. package/dist/chunk-UQ3XI5MY.cjs.map +1 -0
  27. package/dist/chunk-YFRNE2IR.mjs +79 -0
  28. package/dist/chunk-YFRNE2IR.mjs.map +1 -0
  29. package/dist/index.cjs +5042 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +1591 -0
  32. package/dist/index.d.ts +1591 -0
  33. package/dist/index.mjs +4941 -0
  34. package/dist/index.mjs.map +1 -0
  35. package/package.json +86 -0
  36. package/src/components/markdown/MarkdownMessage.tsx +340 -0
  37. package/src/components/markdown/index.ts +5 -0
  38. package/src/index.ts +26 -0
  39. package/src/stores/index.ts +9 -0
  40. package/src/stores/mediaCache.ts +534 -0
  41. package/src/tools/AudioPlayer/README.md +206 -0
  42. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +216 -0
  43. package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +280 -0
  44. package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
  45. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +149 -0
  46. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  47. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  48. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  49. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  50. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  51. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  52. package/src/tools/AudioPlayer/components/index.ts +22 -0
  53. package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +158 -0
  54. package/src/tools/AudioPlayer/context/index.ts +16 -0
  55. package/src/tools/AudioPlayer/effects/index.ts +412 -0
  56. package/src/tools/AudioPlayer/hooks/index.ts +35 -0
  57. package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
  58. package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
  59. package/src/tools/AudioPlayer/hooks/useVisualization.tsx +207 -0
  60. package/src/tools/AudioPlayer/index.ts +133 -0
  61. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  62. package/src/tools/AudioPlayer/types/index.ts +27 -0
  63. package/src/tools/AudioPlayer/utils/debug.ts +14 -0
  64. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  65. package/src/tools/AudioPlayer/utils/index.ts +6 -0
  66. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  67. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  68. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  69. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  70. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  71. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  72. package/src/tools/ImageViewer/README.md +200 -0
  73. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  74. package/src/tools/ImageViewer/components/ImageToolbar.tsx +145 -0
  75. package/src/tools/ImageViewer/components/ImageViewer.tsx +241 -0
  76. package/src/tools/ImageViewer/components/index.ts +7 -0
  77. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  78. package/src/tools/ImageViewer/hooks/useImageLoading.ts +204 -0
  79. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  80. package/src/tools/ImageViewer/index.ts +60 -0
  81. package/src/tools/ImageViewer/types.ts +81 -0
  82. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  83. package/src/tools/ImageViewer/utils/debug.ts +14 -0
  84. package/src/tools/ImageViewer/utils/index.ts +17 -0
  85. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  86. package/src/tools/JsonForm/JsonSchemaForm.tsx +197 -0
  87. package/src/tools/JsonForm/examples/BotConfigExample.tsx +249 -0
  88. package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +161 -0
  89. package/src/tools/JsonForm/index.ts +46 -0
  90. package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +47 -0
  91. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +74 -0
  92. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +107 -0
  93. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +35 -0
  94. package/src/tools/JsonForm/templates/FieldTemplate.tsx +62 -0
  95. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +116 -0
  96. package/src/tools/JsonForm/templates/index.ts +12 -0
  97. package/src/tools/JsonForm/types.ts +83 -0
  98. package/src/tools/JsonForm/utils.ts +213 -0
  99. package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +37 -0
  100. package/src/tools/JsonForm/widgets/ColorWidget.tsx +219 -0
  101. package/src/tools/JsonForm/widgets/NumberWidget.tsx +89 -0
  102. package/src/tools/JsonForm/widgets/SelectWidget.tsx +97 -0
  103. package/src/tools/JsonForm/widgets/SliderWidget.tsx +148 -0
  104. package/src/tools/JsonForm/widgets/SwitchWidget.tsx +35 -0
  105. package/src/tools/JsonForm/widgets/TextWidget.tsx +96 -0
  106. package/src/tools/JsonForm/widgets/index.ts +14 -0
  107. package/src/tools/JsonTree/index.tsx +243 -0
  108. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +213 -0
  109. package/src/tools/LottiePlayer/index.tsx +56 -0
  110. package/src/tools/LottiePlayer/types.ts +108 -0
  111. package/src/tools/LottiePlayer/useLottie.ts +164 -0
  112. package/src/tools/Mermaid/Mermaid.client.tsx +82 -0
  113. package/src/tools/Mermaid/components/MermaidCodeViewer.tsx +95 -0
  114. package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +103 -0
  115. package/src/tools/Mermaid/hooks/index.ts +4 -0
  116. package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +73 -0
  117. package/src/tools/Mermaid/hooks/useMermaidFullscreen.ts +46 -0
  118. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +226 -0
  119. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +29 -0
  120. package/src/tools/Mermaid/index.tsx +44 -0
  121. package/src/tools/Mermaid/utils/mermaid-helpers.ts +33 -0
  122. package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +149 -0
  123. package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +263 -0
  124. package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +125 -0
  125. package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +100 -0
  126. package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +157 -0
  127. package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
  128. package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +173 -0
  129. package/src/tools/OpenapiViewer/components/VersionSelector.tsx +68 -0
  130. package/src/tools/OpenapiViewer/components/index.ts +14 -0
  131. package/src/tools/OpenapiViewer/constants.ts +39 -0
  132. package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +337 -0
  133. package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
  134. package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
  135. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +199 -0
  136. package/src/tools/OpenapiViewer/index.tsx +37 -0
  137. package/src/tools/OpenapiViewer/types.ts +151 -0
  138. package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
  139. package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
  140. package/src/tools/OpenapiViewer/utils/index.ts +9 -0
  141. package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
  142. package/src/tools/PrettyCode/PrettyCode.client.tsx +208 -0
  143. package/src/tools/PrettyCode/index.tsx +47 -0
  144. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  145. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  146. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  147. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  148. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  149. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  150. package/src/tools/VideoPlayer/README.md +264 -0
  151. package/src/tools/VideoPlayer/components/VideoControls.tsx +138 -0
  152. package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +172 -0
  153. package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
  154. package/src/tools/VideoPlayer/components/index.ts +14 -0
  155. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  156. package/src/tools/VideoPlayer/context/index.ts +8 -0
  157. package/src/tools/VideoPlayer/hooks/index.ts +12 -0
  158. package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +70 -0
  159. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +116 -0
  160. package/src/tools/VideoPlayer/index.ts +77 -0
  161. package/src/tools/VideoPlayer/providers/NativeProvider.tsx +284 -0
  162. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +505 -0
  163. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +400 -0
  164. package/src/tools/VideoPlayer/providers/index.ts +8 -0
  165. package/src/tools/VideoPlayer/types/index.ts +38 -0
  166. package/src/tools/VideoPlayer/types/player.ts +116 -0
  167. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  168. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  169. package/src/tools/VideoPlayer/utils/debug.ts +14 -0
  170. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  171. package/src/tools/VideoPlayer/utils/index.ts +12 -0
  172. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  173. package/src/tools/_shared.ts +29 -0
  174. package/src/tools/index.ts +172 -0
@@ -0,0 +1,534 @@
1
+ 'use client';
2
+
3
+ import { create } from 'zustand';
4
+ import { persist, devtools } from 'zustand/middleware';
5
+ import { useShallow } from 'zustand/react/shallow';
6
+ import { createLogger } from '@djangocfg/ui-core/lib';
7
+
8
+ const cacheDebug = createLogger('MediaCache');
9
+
10
+ // Types
11
+ interface BlobUrlEntry {
12
+ url: string;
13
+ refCount: number;
14
+ createdAt: number;
15
+ }
16
+
17
+ interface ImageDimensions {
18
+ width: number;
19
+ height: number;
20
+ }
21
+
22
+ interface VideoMetadata {
23
+ duration: number;
24
+ width: number;
25
+ height: number;
26
+ codec?: string;
27
+ }
28
+
29
+ interface EffectConfig {
30
+ opacity: number;
31
+ scale: number;
32
+ blur: string;
33
+ }
34
+
35
+ /** Video player settings (persisted) */
36
+ interface VideoPlayerSettings {
37
+ volume: number;
38
+ isLooping: boolean;
39
+ }
40
+
41
+ // Stream URL TTL (30 seconds - shorter to avoid stale session/token issues)
42
+ const STREAM_URL_TTL = 30 * 1000;
43
+
44
+ interface MediaCacheState {
45
+ // Blob URL cache (shared by Image, Audio, Video)
46
+ blobUrls: Map<string, BlobUrlEntry>;
47
+
48
+ // Image-specific
49
+ imageDimensions: Map<string, ImageDimensions>;
50
+
51
+ // Audio-specific
52
+ audioPlaybackPositions: Map<string, number>;
53
+ audioEffectConfigs: Map<string, EffectConfig>;
54
+
55
+ // Video-specific
56
+ videoStreamUrls: Map<string, { url: string; timestamp: number }>;
57
+ videoPosterUrls: Map<string, string>;
58
+ videoPlaybackPositions: Map<string, number>;
59
+ videoMetadata: Map<string, VideoMetadata>;
60
+ videoPlayerSettings: VideoPlayerSettings;
61
+ }
62
+
63
+ interface MediaCacheActions {
64
+ // Blob URL management (shared)
65
+ getOrCreateBlobUrl: (
66
+ key: string,
67
+ content: ArrayBuffer,
68
+ mimeType: string
69
+ ) => string;
70
+ releaseBlobUrl: (key: string) => void;
71
+ hasBlobUrl: (key: string) => boolean;
72
+
73
+ // Image actions
74
+ cacheDimensions: (src: string, dims: ImageDimensions) => void;
75
+ getDimensions: (src: string) => ImageDimensions | null;
76
+
77
+ // Audio actions
78
+ saveAudioPosition: (uri: string, time: number) => void;
79
+ getAudioPosition: (uri: string) => number | null;
80
+ getEffectConfig: (key: string) => EffectConfig | null;
81
+ cacheEffectConfig: (key: string, config: EffectConfig) => void;
82
+
83
+ // Video actions
84
+ getOrCreateStreamUrl: (
85
+ sessionId: string,
86
+ path: string,
87
+ generator: (sessionId: string, path: string) => string
88
+ ) => string;
89
+ getPosterUrl: (title: string) => string | null;
90
+ cachePosterUrl: (title: string, url: string) => void;
91
+ saveVideoPosition: (key: string, time: number) => void;
92
+ getVideoPosition: (key: string) => number | null;
93
+ cacheVideoMetadata: (url: string, meta: VideoMetadata) => void;
94
+ getVideoMetadata: (url: string) => VideoMetadata | null;
95
+ invalidateSession: (sessionId: string) => void;
96
+ getVideoPlayerSettings: () => VideoPlayerSettings;
97
+ saveVideoPlayerSettings: (settings: Partial<VideoPlayerSettings>) => void;
98
+
99
+ // Global
100
+ clearCache: () => void;
101
+ getCacheStats: () => {
102
+ blobUrls: number;
103
+ dimensions: number;
104
+ audioPositions: number;
105
+ videoPositions: number;
106
+ };
107
+ }
108
+
109
+ type MediaCacheStore = MediaCacheState & MediaCacheActions;
110
+
111
+ // Initial state
112
+ const DEFAULT_VIDEO_SETTINGS: VideoPlayerSettings = {
113
+ volume: 1,
114
+ isLooping: false,
115
+ };
116
+
117
+ const initialState: MediaCacheState = {
118
+ blobUrls: new Map(),
119
+ imageDimensions: new Map(),
120
+ audioPlaybackPositions: new Map(),
121
+ audioEffectConfigs: new Map(),
122
+ videoStreamUrls: new Map(),
123
+ videoPosterUrls: new Map(),
124
+ videoPlaybackPositions: new Map(),
125
+ videoMetadata: new Map(),
126
+ videoPlayerSettings: DEFAULT_VIDEO_SETTINGS,
127
+ };
128
+
129
+ export const useMediaCacheStore = create<MediaCacheStore>()(
130
+ devtools(
131
+ persist(
132
+ (set, get) => ({
133
+ ...initialState,
134
+
135
+ // ========== Blob URL Management ==========
136
+
137
+ getOrCreateBlobUrl: (key, content, mimeType) => {
138
+ const existing = get().blobUrls.get(key);
139
+ if (existing) {
140
+ // Increment ref count
141
+ cacheDebug.debug(`Blob URL reused: ${key}`, { refCount: existing.refCount + 1 });
142
+ set(
143
+ (state) => ({
144
+ blobUrls: new Map(state.blobUrls).set(key, {
145
+ ...existing,
146
+ refCount: existing.refCount + 1,
147
+ }),
148
+ }),
149
+ false,
150
+ 'blobUrl/incrementRef'
151
+ );
152
+ return existing.url;
153
+ }
154
+
155
+ // Create new blob URL
156
+ const blob = new Blob([content], { type: mimeType });
157
+ const url = URL.createObjectURL(blob);
158
+ const sizeMB = (content.byteLength / 1024 / 1024).toFixed(2);
159
+ cacheDebug.debug(`Blob URL created: ${key}`, { mimeType, size: `${sizeMB}MB` });
160
+ set(
161
+ (state) => ({
162
+ blobUrls: new Map(state.blobUrls).set(key, {
163
+ url,
164
+ refCount: 1,
165
+ createdAt: Date.now(),
166
+ }),
167
+ }),
168
+ false,
169
+ 'blobUrl/create'
170
+ );
171
+ return url;
172
+ },
173
+
174
+ releaseBlobUrl: (key) => {
175
+ const entry = get().blobUrls.get(key);
176
+ if (!entry) return;
177
+
178
+ if (entry.refCount <= 1) {
179
+ // Last reference - revoke and remove
180
+ cacheDebug.debug(`Blob URL revoked: ${key}`);
181
+ URL.revokeObjectURL(entry.url);
182
+ set(
183
+ (state) => {
184
+ const newMap = new Map(state.blobUrls);
185
+ newMap.delete(key);
186
+ return { blobUrls: newMap };
187
+ },
188
+ false,
189
+ 'blobUrl/revoke'
190
+ );
191
+ } else {
192
+ // Decrement ref count
193
+ cacheDebug.debug(`Blob URL released: ${key}`, { refCount: entry.refCount - 1 });
194
+ set(
195
+ (state) => ({
196
+ blobUrls: new Map(state.blobUrls).set(key, {
197
+ ...entry,
198
+ refCount: entry.refCount - 1,
199
+ }),
200
+ }),
201
+ false,
202
+ 'blobUrl/decrementRef'
203
+ );
204
+ }
205
+ },
206
+
207
+ hasBlobUrl: (key) => get().blobUrls.has(key),
208
+
209
+ // ========== Image Cache ==========
210
+
211
+ cacheDimensions: (src, dims) => {
212
+ set(
213
+ (state) => ({
214
+ imageDimensions: new Map(state.imageDimensions).set(src, dims),
215
+ }),
216
+ false,
217
+ 'image/cacheDimensions'
218
+ );
219
+ },
220
+
221
+ getDimensions: (src) => get().imageDimensions.get(src) ?? null,
222
+
223
+ // ========== Audio Cache ==========
224
+
225
+ saveAudioPosition: (uri, time) => {
226
+ set(
227
+ (state) => ({
228
+ audioPlaybackPositions: new Map(state.audioPlaybackPositions).set(
229
+ uri,
230
+ time
231
+ ),
232
+ }),
233
+ false,
234
+ 'audio/savePosition'
235
+ );
236
+ },
237
+
238
+ getAudioPosition: (uri) =>
239
+ get().audioPlaybackPositions.get(uri) ?? null,
240
+
241
+ getEffectConfig: (key) => get().audioEffectConfigs.get(key) ?? null,
242
+
243
+ cacheEffectConfig: (key, config) => {
244
+ set(
245
+ (state) => ({
246
+ audioEffectConfigs: new Map(state.audioEffectConfigs).set(
247
+ key,
248
+ config
249
+ ),
250
+ }),
251
+ false,
252
+ 'audio/cacheEffectConfig'
253
+ );
254
+ },
255
+
256
+ // ========== Video Cache ==========
257
+
258
+ getOrCreateStreamUrl: (sessionId, path, generator) => {
259
+ const key = `${sessionId}:${path}`;
260
+ const cached = get().videoStreamUrls.get(key);
261
+
262
+ // Return if fresh
263
+ if (cached && Date.now() - cached.timestamp < STREAM_URL_TTL) {
264
+ return cached.url;
265
+ }
266
+
267
+ // Generate and cache
268
+ const url = generator(sessionId, path);
269
+ set(
270
+ (state) => ({
271
+ videoStreamUrls: new Map(state.videoStreamUrls).set(key, {
272
+ url,
273
+ timestamp: Date.now(),
274
+ }),
275
+ }),
276
+ false,
277
+ 'video/cacheStreamUrl'
278
+ );
279
+ return url;
280
+ },
281
+
282
+ getPosterUrl: (title) => get().videoPosterUrls.get(title) ?? null,
283
+
284
+ cachePosterUrl: (title, url) => {
285
+ set(
286
+ (state) => ({
287
+ videoPosterUrls: new Map(state.videoPosterUrls).set(title, url),
288
+ }),
289
+ false,
290
+ 'video/cachePosterUrl'
291
+ );
292
+ },
293
+
294
+ saveVideoPosition: (key, time) => {
295
+ set(
296
+ (state) => ({
297
+ videoPlaybackPositions: new Map(state.videoPlaybackPositions).set(
298
+ key,
299
+ time
300
+ ),
301
+ }),
302
+ false,
303
+ 'video/savePosition'
304
+ );
305
+ },
306
+
307
+ getVideoPosition: (key) =>
308
+ get().videoPlaybackPositions.get(key) ?? null,
309
+
310
+ cacheVideoMetadata: (url, meta) => {
311
+ set(
312
+ (state) => ({
313
+ videoMetadata: new Map(state.videoMetadata).set(url, meta),
314
+ }),
315
+ false,
316
+ 'video/cacheMetadata'
317
+ );
318
+ },
319
+
320
+ getVideoMetadata: (url) => get().videoMetadata.get(url) ?? null,
321
+
322
+ invalidateSession: (sessionId) => {
323
+ set(
324
+ (state) => {
325
+ const newUrls = new Map(state.videoStreamUrls);
326
+ for (const [key] of newUrls) {
327
+ if (key.startsWith(`${sessionId}:`)) {
328
+ newUrls.delete(key);
329
+ }
330
+ }
331
+ return { videoStreamUrls: newUrls };
332
+ },
333
+ false,
334
+ 'video/invalidateSession'
335
+ );
336
+ },
337
+
338
+ getVideoPlayerSettings: () => get().videoPlayerSettings,
339
+
340
+ saveVideoPlayerSettings: (settings) => {
341
+ set(
342
+ (state) => ({
343
+ videoPlayerSettings: { ...state.videoPlayerSettings, ...settings },
344
+ }),
345
+ false,
346
+ 'video/savePlayerSettings'
347
+ );
348
+ },
349
+
350
+ // ========== Global ==========
351
+
352
+ clearCache: () => {
353
+ const stats = get().getCacheStats();
354
+ cacheDebug.info('Clearing cache', stats);
355
+ // Revoke all blob URLs before clearing
356
+ get().blobUrls.forEach(({ url }) => URL.revokeObjectURL(url));
357
+ set(initialState, false, 'clearCache');
358
+ },
359
+
360
+ getCacheStats: () => ({
361
+ blobUrls: get().blobUrls.size,
362
+ dimensions: get().imageDimensions.size,
363
+ audioPositions: get().audioPlaybackPositions.size,
364
+ videoPositions: get().videoPlaybackPositions.size,
365
+ }),
366
+ }),
367
+ {
368
+ name: 'media-cache-storage',
369
+ // Only persist playback positions, poster URLs, and player settings
370
+ partialize: (state) => ({
371
+ audioPlaybackPositions: Array.from(
372
+ state.audioPlaybackPositions.entries()
373
+ ),
374
+ videoPlaybackPositions: Array.from(
375
+ state.videoPlaybackPositions.entries()
376
+ ),
377
+ videoPosterUrls: Array.from(state.videoPosterUrls.entries()),
378
+ videoPlayerSettings: state.videoPlayerSettings,
379
+ }),
380
+ // Rehydrate Maps from arrays
381
+ onRehydrateStorage: () => (state) => {
382
+ if (state) {
383
+ // Type assertion for persisted data
384
+ const persistedAudio = state.audioPlaybackPositions as unknown;
385
+ const persistedVideo = state.videoPlaybackPositions as unknown;
386
+ const persistedPosters = state.videoPosterUrls as unknown;
387
+ const persistedSettings = state.videoPlayerSettings as unknown;
388
+
389
+ state.audioPlaybackPositions = new Map(
390
+ Array.isArray(persistedAudio)
391
+ ? (persistedAudio as [string, number][])
392
+ : []
393
+ );
394
+ state.videoPlaybackPositions = new Map(
395
+ Array.isArray(persistedVideo)
396
+ ? (persistedVideo as [string, number][])
397
+ : []
398
+ );
399
+ state.videoPosterUrls = new Map(
400
+ Array.isArray(persistedPosters)
401
+ ? (persistedPosters as [string, string][])
402
+ : []
403
+ );
404
+ // Merge persisted settings with defaults (handles missing fields)
405
+ state.videoPlayerSettings = {
406
+ ...DEFAULT_VIDEO_SETTINGS,
407
+ ...(persistedSettings && typeof persistedSettings === 'object'
408
+ ? (persistedSettings as Partial<VideoPlayerSettings>)
409
+ : {}),
410
+ };
411
+ }
412
+ },
413
+ }
414
+ ),
415
+ { name: 'MediaCacheStore' }
416
+ )
417
+ );
418
+
419
+ // ========== Selective Hooks ==========
420
+
421
+ /**
422
+ * Hook for image-related cache operations
423
+ * Uses useShallow to prevent infinite re-renders
424
+ */
425
+ export const useImageCache = () =>
426
+ useMediaCacheStore(
427
+ useShallow((state) => ({
428
+ getOrCreateBlobUrl: state.getOrCreateBlobUrl,
429
+ releaseBlobUrl: state.releaseBlobUrl,
430
+ hasBlobUrl: state.hasBlobUrl,
431
+ cacheDimensions: state.cacheDimensions,
432
+ getDimensions: state.getDimensions,
433
+ }))
434
+ );
435
+
436
+ /**
437
+ * Hook for audio-related cache operations
438
+ * Uses useShallow to prevent infinite re-renders
439
+ */
440
+ export const useAudioCache = () =>
441
+ useMediaCacheStore(
442
+ useShallow((state) => ({
443
+ getOrCreateBlobUrl: state.getOrCreateBlobUrl,
444
+ releaseBlobUrl: state.releaseBlobUrl,
445
+ hasBlobUrl: state.hasBlobUrl,
446
+ saveAudioPosition: state.saveAudioPosition,
447
+ getAudioPosition: state.getAudioPosition,
448
+ getEffectConfig: state.getEffectConfig,
449
+ cacheEffectConfig: state.cacheEffectConfig,
450
+ }))
451
+ );
452
+
453
+ /**
454
+ * Hook for video-related cache operations
455
+ * Uses useShallow to prevent infinite re-renders
456
+ */
457
+ export const useVideoCache = () =>
458
+ useMediaCacheStore(
459
+ useShallow((state) => ({
460
+ getOrCreateBlobUrl: state.getOrCreateBlobUrl,
461
+ releaseBlobUrl: state.releaseBlobUrl,
462
+ hasBlobUrl: state.hasBlobUrl,
463
+ getOrCreateStreamUrl: state.getOrCreateStreamUrl,
464
+ getPosterUrl: state.getPosterUrl,
465
+ cachePosterUrl: state.cachePosterUrl,
466
+ saveVideoPosition: state.saveVideoPosition,
467
+ getVideoPosition: state.getVideoPosition,
468
+ cacheVideoMetadata: state.cacheVideoMetadata,
469
+ getVideoMetadata: state.getVideoMetadata,
470
+ invalidateSession: state.invalidateSession,
471
+ getVideoPlayerSettings: state.getVideoPlayerSettings,
472
+ saveVideoPlayerSettings: state.saveVideoPlayerSettings,
473
+ }))
474
+ );
475
+
476
+ /**
477
+ * Hook for video player settings only
478
+ * Returns current settings and save function
479
+ */
480
+ export const useVideoPlayerSettings = () =>
481
+ useMediaCacheStore(
482
+ useShallow((state) => ({
483
+ settings: state.videoPlayerSettings,
484
+ saveSettings: state.saveVideoPlayerSettings,
485
+ }))
486
+ );
487
+
488
+ /**
489
+ * Hook for blob URL cleanup on unmount
490
+ */
491
+ export function useBlobUrlCleanup(key: string | null) {
492
+ const releaseBlobUrl = useMediaCacheStore((s) => s.releaseBlobUrl);
493
+
494
+ // Using inline effect to avoid importing useEffect in store
495
+ if (typeof window !== 'undefined') {
496
+ // eslint-disable-next-line react-hooks/rules-of-hooks
497
+ const { useEffect } = require('react');
498
+ // eslint-disable-next-line react-hooks/rules-of-hooks
499
+ useEffect(() => {
500
+ return () => {
501
+ if (key) {
502
+ releaseBlobUrl(key);
503
+ }
504
+ };
505
+ }, [key, releaseBlobUrl]);
506
+ }
507
+ }
508
+
509
+ // ========== Utilities ==========
510
+
511
+ /**
512
+ * Generate a cache key from ArrayBuffer content
513
+ * Uses first and last 1KB + length for fast hashing
514
+ */
515
+ export function generateContentKey(content: ArrayBuffer): string {
516
+ const arr = new Uint8Array(content);
517
+ const len = arr.length;
518
+
519
+ // Take first 1KB
520
+ const start = arr.slice(0, Math.min(1024, len));
521
+ // Take last 1KB
522
+ const end = arr.slice(Math.max(0, len - 1024));
523
+
524
+ // Simple hash from bytes
525
+ let hash = len;
526
+ for (let i = 0; i < start.length; i++) {
527
+ hash = ((hash << 5) - hash + start[i]) | 0;
528
+ }
529
+ for (let i = 0; i < end.length; i++) {
530
+ hash = ((hash << 5) - hash + end[i]) | 0;
531
+ }
532
+
533
+ return `blob-${len}-${hash.toString(16)}`;
534
+ }