@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
@@ -0,0 +1,401 @@
1
+ /**
2
+ * StreamProvider - HTTP Range streaming and Blob video player
3
+ * Supports:
4
+ * - HTTP Range requests with authorization (for large files)
5
+ * - Blob/ArrayBuffer sources
6
+ * - Data URL sources
7
+ * - Fill parent container mode
8
+ * - Custom error fallback
9
+ */
10
+
11
+ 'use client';
12
+
13
+ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
14
+
15
+ import { cn } from '@djangocfg/ui-core/lib';
16
+ import { Preloader, AspectRatio } from '@djangocfg/ui-core';
17
+ import { useVideoCache, generateContentKey } from '../../../stores/mediaCache';
18
+
19
+ import type { StreamProviderProps, VideoPlayerRef, StreamSource, BlobSource, DataUrlSource, ErrorFallbackProps } from '../types';
20
+
21
+ /** Default error fallback UI */
22
+ function DefaultErrorFallback({ error }: ErrorFallbackProps) {
23
+ return (
24
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-4 text-white">
25
+ <svg
26
+ className="w-16 h-16 text-muted-foreground"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ viewBox="0 0 24 24"
30
+ >
31
+ <path
32
+ strokeLinecap="round"
33
+ strokeLinejoin="round"
34
+ strokeWidth={2}
35
+ d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
36
+ />
37
+ </svg>
38
+ <p className="text-lg">{error || 'Video cannot be previewed'}</p>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
44
+ (
45
+ {
46
+ source,
47
+ aspectRatio = 16 / 9,
48
+ autoPlay = false,
49
+ muted = false,
50
+ loop = false,
51
+ playsInline = true,
52
+ preload = 'metadata',
53
+ controls = true,
54
+ disableContextMenu = false,
55
+ showPreloader = true,
56
+ preloaderTimeout = 10000,
57
+ className,
58
+ videoClassName,
59
+ errorFallback,
60
+ onPlay,
61
+ onPause,
62
+ onEnded,
63
+ onError,
64
+ onLoadStart,
65
+ onCanPlay,
66
+ onTimeUpdate,
67
+ onBufferProgress,
68
+ },
69
+ ref
70
+ ) => {
71
+ const [videoUrl, setVideoUrl] = useState<string | null>(null);
72
+ const [isLoading, setIsLoading] = useState(true);
73
+ const [hasError, setHasError] = useState(false);
74
+ const [errorMessage, setErrorMessage] = useState<string>('Video cannot be previewed');
75
+ const videoRef = useRef<HTMLVideoElement>(null);
76
+ const contentKeyRef = useRef<string | null>(null);
77
+ const lastSavedTimeRef = useRef<number>(0);
78
+
79
+ // Cache hooks
80
+ const {
81
+ getOrCreateBlobUrl,
82
+ releaseBlobUrl,
83
+ getOrCreateStreamUrl,
84
+ saveVideoPosition,
85
+ getVideoPosition,
86
+ } = useVideoCache();
87
+
88
+ // Retry function for error fallback
89
+ const retry = useCallback(() => {
90
+ setHasError(false);
91
+ setIsLoading(true);
92
+ // Re-trigger source effect by forcing state update
93
+ const video = videoRef.current;
94
+ if (video && videoUrl) {
95
+ video.load();
96
+ }
97
+ }, [videoUrl]);
98
+
99
+ // Expose video element methods via ref
100
+ useImperativeHandle(
101
+ ref,
102
+ () => ({
103
+ play: () => videoRef.current?.play(),
104
+ pause: () => videoRef.current?.pause(),
105
+ togglePlay: () => {
106
+ const video = videoRef.current;
107
+ if (video) {
108
+ video.paused ? video.play() : video.pause();
109
+ }
110
+ },
111
+ seekTo: (time: number) => {
112
+ if (videoRef.current) videoRef.current.currentTime = time;
113
+ },
114
+ setVolume: (volume: number) => {
115
+ if (videoRef.current) videoRef.current.volume = Math.max(0, Math.min(1, volume));
116
+ },
117
+ toggleMute: () => {
118
+ if (videoRef.current) videoRef.current.muted = !videoRef.current.muted;
119
+ },
120
+ enterFullscreen: () => videoRef.current?.requestFullscreen(),
121
+ exitFullscreen: () => document.exitFullscreen(),
122
+ get currentTime() {
123
+ return videoRef.current?.currentTime ?? 0;
124
+ },
125
+ get duration() {
126
+ return videoRef.current?.duration ?? 0;
127
+ },
128
+ get paused() {
129
+ return videoRef.current?.paused ?? true;
130
+ },
131
+ }),
132
+ []
133
+ );
134
+
135
+ // Create video URL based on source type with caching
136
+ useEffect(() => {
137
+ // Cleanup previous blob URL from cache
138
+ if (contentKeyRef.current) {
139
+ releaseBlobUrl(contentKeyRef.current);
140
+ contentKeyRef.current = null;
141
+ }
142
+
143
+ setHasError(false);
144
+ setIsLoading(true);
145
+
146
+ switch (source.type) {
147
+ case 'stream': {
148
+ const streamSource = source as StreamSource;
149
+ // Use cached stream URL
150
+ const url = getOrCreateStreamUrl(
151
+ streamSource.sessionId,
152
+ streamSource.path,
153
+ streamSource.getStreamUrl
154
+ );
155
+ setVideoUrl(url);
156
+ break;
157
+ }
158
+
159
+ case 'blob': {
160
+ const blobSource = source as BlobSource;
161
+ // Generate content key for caching
162
+ const contentKey = generateContentKey(blobSource.data);
163
+ contentKeyRef.current = contentKey;
164
+ // Use cached blob URL
165
+ const url = getOrCreateBlobUrl(
166
+ contentKey,
167
+ blobSource.data,
168
+ blobSource.mimeType || 'video/mp4'
169
+ );
170
+ setVideoUrl(url);
171
+ break;
172
+ }
173
+
174
+ case 'data-url': {
175
+ const dataUrlSource = source as DataUrlSource;
176
+ setVideoUrl(dataUrlSource.data);
177
+ break;
178
+ }
179
+
180
+ default:
181
+ setVideoUrl(null);
182
+ setHasError(true);
183
+ setErrorMessage('Invalid video source');
184
+ }
185
+
186
+ return () => {
187
+ if (contentKeyRef.current) {
188
+ releaseBlobUrl(contentKeyRef.current);
189
+ contentKeyRef.current = null;
190
+ }
191
+ };
192
+ }, [source, getOrCreateBlobUrl, getOrCreateStreamUrl, releaseBlobUrl]);
193
+
194
+ // Get source key for position caching
195
+ const getSourceKey = useCallback(() => {
196
+ switch (source.type) {
197
+ case 'stream':
198
+ return `stream:${(source as StreamSource).sessionId}:${(source as StreamSource).path}`;
199
+ case 'blob':
200
+ return contentKeyRef.current ? `blob:${contentKeyRef.current}` : null;
201
+ case 'data-url':
202
+ return `data:${(source as DataUrlSource).data.slice(0, 50)}`;
203
+ default:
204
+ return null;
205
+ }
206
+ }, [source]);
207
+
208
+ // Restore cached playback position when video is ready
209
+ const handleCanPlay = useCallback(() => {
210
+ setIsLoading(false);
211
+
212
+ // Restore position from cache
213
+ const sourceKey = getSourceKey();
214
+ if (sourceKey && videoRef.current) {
215
+ const cachedPosition = getVideoPosition(sourceKey);
216
+ if (cachedPosition && cachedPosition > 0) {
217
+ const duration = videoRef.current.duration;
218
+ // Only restore if position is valid (not at the end)
219
+ if (cachedPosition < duration - 1) {
220
+ videoRef.current.currentTime = cachedPosition;
221
+ }
222
+ }
223
+ }
224
+
225
+ onCanPlay?.();
226
+ }, [getSourceKey, getVideoPosition, onCanPlay]);
227
+
228
+ // Save playback position periodically
229
+ const handleTimeUpdate = useCallback(() => {
230
+ const video = videoRef.current;
231
+ if (!video) return;
232
+
233
+ // Save position every 5 seconds
234
+ const sourceKey = getSourceKey();
235
+ if (sourceKey && video.currentTime > 0) {
236
+ const timeSinceLastSave = video.currentTime - lastSavedTimeRef.current;
237
+ if (timeSinceLastSave >= 5 || timeSinceLastSave < 0) {
238
+ saveVideoPosition(sourceKey, video.currentTime);
239
+ lastSavedTimeRef.current = video.currentTime;
240
+ }
241
+ }
242
+
243
+ onTimeUpdate?.(video.currentTime, video.duration);
244
+ }, [getSourceKey, saveVideoPosition, onTimeUpdate]);
245
+
246
+ // Save position on pause
247
+ const handlePause = useCallback(() => {
248
+ const video = videoRef.current;
249
+ const sourceKey = getSourceKey();
250
+ if (sourceKey && video && video.currentTime > 0) {
251
+ saveVideoPosition(sourceKey, video.currentTime);
252
+ lastSavedTimeRef.current = video.currentTime;
253
+ }
254
+ onPause?.();
255
+ }, [getSourceKey, saveVideoPosition, onPause]);
256
+
257
+ // Handle buffer progress
258
+ const handleProgress = useCallback(() => {
259
+ const video = videoRef.current;
260
+ if (!video || !onBufferProgress) return;
261
+
262
+ // Get the buffered time ranges
263
+ if (video.buffered.length > 0) {
264
+ // Get the end of the last buffered range
265
+ const bufferedEnd = video.buffered.end(video.buffered.length - 1);
266
+ const duration = video.duration;
267
+
268
+ if (duration > 0 && !isNaN(bufferedEnd)) {
269
+ onBufferProgress(bufferedEnd, duration);
270
+ }
271
+ }
272
+ }, [onBufferProgress]);
273
+
274
+ // Preloader timeout
275
+ useEffect(() => {
276
+ if (!showPreloader || !isLoading) return;
277
+
278
+ const timeout = setTimeout(() => {
279
+ setIsLoading(false);
280
+ }, preloaderTimeout);
281
+
282
+ return () => clearTimeout(timeout);
283
+ }, [showPreloader, isLoading, preloaderTimeout]);
284
+
285
+ const handleContextMenu = (e: React.MouseEvent) => {
286
+ if (disableContextMenu) {
287
+ e.preventDefault();
288
+ }
289
+ };
290
+
291
+ const handleLoadedData = () => {
292
+ setIsLoading(false);
293
+ };
294
+
295
+ const handleError = () => {
296
+ setIsLoading(false);
297
+ setHasError(true);
298
+ setErrorMessage('Failed to load video');
299
+ onError?.('Video playback error');
300
+ };
301
+
302
+ // Determine if we should use AspectRatio wrapper or fill mode
303
+ const isFillMode = aspectRatio === 'fill';
304
+ const computedAspectRatio = aspectRatio === 'auto' || aspectRatio === 'fill' ? undefined : aspectRatio;
305
+
306
+ // Render error fallback
307
+ const renderErrorFallback = () => {
308
+ const fallbackProps: ErrorFallbackProps = { error: errorMessage, retry };
309
+
310
+ if (typeof errorFallback === 'function') {
311
+ return errorFallback(fallbackProps);
312
+ }
313
+
314
+ if (errorFallback) {
315
+ return errorFallback;
316
+ }
317
+
318
+ return <DefaultErrorFallback {...fallbackProps} />;
319
+ };
320
+
321
+ // Error state
322
+ if (!videoUrl || hasError) {
323
+ if (isFillMode) {
324
+ return (
325
+ <div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
326
+ {renderErrorFallback()}
327
+ </div>
328
+ );
329
+ }
330
+
331
+ return (
332
+ <div className={cn('relative overflow-hidden bg-black', className)}>
333
+ <AspectRatio ratio={computedAspectRatio}>
334
+ {renderErrorFallback()}
335
+ </AspectRatio>
336
+ </div>
337
+ );
338
+ }
339
+
340
+ // Video content
341
+ const videoContent = (
342
+ <>
343
+ {/* Loading indicator */}
344
+ {showPreloader && isLoading && (
345
+ <div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10">
346
+ <Preloader size="lg" spinnerClassName="text-white" />
347
+ </div>
348
+ )}
349
+
350
+ {/* Video element */}
351
+ <video
352
+ ref={videoRef}
353
+ src={videoUrl}
354
+ className={cn(
355
+ 'w-full h-full object-contain',
356
+ isLoading && 'opacity-0',
357
+ videoClassName
358
+ )}
359
+ autoPlay={autoPlay}
360
+ muted={muted}
361
+ loop={loop}
362
+ playsInline={playsInline}
363
+ preload={preload}
364
+ controls={controls}
365
+ crossOrigin="anonymous"
366
+ poster={source.poster}
367
+ onContextMenu={handleContextMenu}
368
+ onLoadStart={onLoadStart}
369
+ onCanPlay={handleCanPlay}
370
+ onLoadedData={handleLoadedData}
371
+ onPlay={onPlay}
372
+ onPause={handlePause}
373
+ onEnded={onEnded}
374
+ onError={handleError}
375
+ onTimeUpdate={handleTimeUpdate}
376
+ onProgress={handleProgress}
377
+ />
378
+ </>
379
+ );
380
+
381
+ // Fill mode - no AspectRatio wrapper
382
+ if (isFillMode) {
383
+ return (
384
+ <div className={cn('relative w-full h-full overflow-hidden bg-black', className)}>
385
+ {videoContent}
386
+ </div>
387
+ );
388
+ }
389
+
390
+ // Normal mode with AspectRatio
391
+ return (
392
+ <div className={cn('relative overflow-hidden bg-black', className)}>
393
+ <AspectRatio ratio={computedAspectRatio}>
394
+ {videoContent}
395
+ </AspectRatio>
396
+ </div>
397
+ );
398
+ }
399
+ );
400
+
401
+ StreamProvider.displayName = 'StreamProvider';