@djangocfg/ui-nextjs 2.1.80 → 2.1.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +8 -5
- package/src/tools/AudioPlayer/hooks/useAudioSource.ts +17 -2
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +33 -9
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +13 -6
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +38 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.82",
|
|
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.
|
|
62
|
-
"@djangocfg/ui-core": "^2.1.
|
|
61
|
+
"@djangocfg/api": "^2.1.82",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.82",
|
|
63
63
|
"@types/react": "^19.1.0",
|
|
64
64
|
"@types/react-dom": "^19.1.0",
|
|
65
65
|
"consola": "^3.4.2",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"wavesurfer.js": "^7.12.1"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
113
|
+
"@djangocfg/typescript-config": "^2.1.82",
|
|
114
114
|
"@types/node": "^24.7.2",
|
|
115
115
|
"eslint": "^9.37.0",
|
|
116
116
|
"tailwindcss-animate": "1.0.7",
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { useWavesurfer } from '@wavesurfer/react';
|
|
20
20
|
import type { AudioContextState, AudioSource, WaveformOptions } from '../types';
|
|
21
21
|
import { useSharedWebAudio, useAudioAnalysis, useAudioSource } from '../hooks';
|
|
22
|
-
import {
|
|
22
|
+
import { useMediaCacheStore } from '../../../stores/mediaCache';
|
|
23
23
|
import { audioDebug } from '../utils/debug';
|
|
24
24
|
|
|
25
25
|
// =============================================================================
|
|
@@ -57,8 +57,9 @@ export function AudioProvider({
|
|
|
57
57
|
containerRef,
|
|
58
58
|
children,
|
|
59
59
|
}: AudioProviderProps) {
|
|
60
|
-
//
|
|
61
|
-
const
|
|
60
|
+
// Get stable function references from store
|
|
61
|
+
const saveAudioPosition = useMediaCacheStore.getState().saveAudioPosition;
|
|
62
|
+
const getAudioPosition = useMediaCacheStore.getState().getAudioPosition;
|
|
62
63
|
const lastSavedTimeRef = useRef<number>(0);
|
|
63
64
|
|
|
64
65
|
// Handle prefetch if enabled (for streaming URLs)
|
|
@@ -149,7 +150,8 @@ export function AudioProvider({
|
|
|
149
150
|
}
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
154
|
+
}, [isReady, wavesurfer, source.uri]);
|
|
153
155
|
|
|
154
156
|
// Save playback position periodically and on pause
|
|
155
157
|
useEffect(() => {
|
|
@@ -169,7 +171,8 @@ export function AudioProvider({
|
|
|
169
171
|
saveAudioPosition(source.uri, currentTime);
|
|
170
172
|
lastSavedTimeRef.current = currentTime;
|
|
171
173
|
}
|
|
172
|
-
|
|
174
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
175
|
+
}, [isPlaying, currentTime, source.uri]);
|
|
173
176
|
|
|
174
177
|
// Derived state
|
|
175
178
|
const duration = wavesurfer?.getDuration() ?? 0;
|
|
@@ -58,15 +58,30 @@ export function useAudioSource(source: AudioSource): UseAudioSourceResult {
|
|
|
58
58
|
|
|
59
59
|
const response = await fetch(source.uri, {
|
|
60
60
|
signal: abortController.signal,
|
|
61
|
+
headers: {
|
|
62
|
+
// Request full file - some servers require Range header
|
|
63
|
+
'Range': 'bytes=0-',
|
|
64
|
+
},
|
|
61
65
|
});
|
|
62
66
|
|
|
67
|
+
// Accept 200 OK or 206 Partial Content (response.ok covers both)
|
|
63
68
|
if (!response.ok) {
|
|
64
69
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
// Get content length for progress tracking
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
// For Range requests, use Content-Range header (format: "bytes 0-1234/5678")
|
|
74
|
+
let totalBytes = 0;
|
|
75
|
+
const contentRange = response.headers.get('Content-Range');
|
|
76
|
+
if (contentRange) {
|
|
77
|
+
const match = contentRange.match(/\/(\d+)$/);
|
|
78
|
+
if (match) {
|
|
79
|
+
totalBytes = parseInt(match[1], 10);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
const contentLength = response.headers.get('Content-Length');
|
|
83
|
+
totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
|
|
84
|
+
}
|
|
70
85
|
|
|
71
86
|
if (!response.body) {
|
|
72
87
|
// Fallback for browsers without ReadableStream
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useState, useEffect, useRef } from 'react';
|
|
8
|
-
import {
|
|
8
|
+
import { useMediaCacheStore, generateContentKey } from '../../../stores/mediaCache';
|
|
9
9
|
import { createLQIP, MAX_IMAGE_SIZE, WARNING_IMAGE_SIZE, PROGRESSIVE_LOADING_THRESHOLD, imageDebug } from '../utils';
|
|
10
10
|
|
|
11
11
|
// =============================================================================
|
|
@@ -50,7 +50,9 @@ export interface UseImageLoadingReturn {
|
|
|
50
50
|
export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadingReturn {
|
|
51
51
|
const { content, mimeType, src: directSrc } = options;
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
// Get stable function references from store (not from hook to avoid re-renders)
|
|
54
|
+
const getOrCreateBlobUrl = useMediaCacheStore.getState().getOrCreateBlobUrl;
|
|
55
|
+
const releaseBlobUrl = useMediaCacheStore.getState().releaseBlobUrl;
|
|
54
56
|
|
|
55
57
|
const [src, setSrc] = useState<string | null>(null);
|
|
56
58
|
const [lqip, setLqip] = useState<string | null>(null);
|
|
@@ -58,6 +60,7 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
58
60
|
const [error, setError] = useState<string | null>(null);
|
|
59
61
|
|
|
60
62
|
const contentKeyRef = useRef<string | null>(null);
|
|
63
|
+
const isMountedRef = useRef(true);
|
|
61
64
|
|
|
62
65
|
// Calculate size and flags
|
|
63
66
|
const size = content ? (typeof content === 'string' ? content.length : content.byteLength) : 0;
|
|
@@ -65,6 +68,19 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
65
68
|
const hasContent = directSrc ? true : size > 0;
|
|
66
69
|
const useProgressiveLoading = directSrc ? false : size > PROGRESSIVE_LOADING_THRESHOLD;
|
|
67
70
|
|
|
71
|
+
// Track unmount for cleanup
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
isMountedRef.current = true;
|
|
74
|
+
return () => {
|
|
75
|
+
isMountedRef.current = false;
|
|
76
|
+
// Release blob URL only on actual unmount
|
|
77
|
+
if (contentKeyRef.current) {
|
|
78
|
+
useMediaCacheStore.getState().releaseBlobUrl(contentKeyRef.current);
|
|
79
|
+
contentKeyRef.current = null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
68
84
|
// Create blob URL with caching and size validation
|
|
69
85
|
useEffect(() => {
|
|
70
86
|
// Reset error state
|
|
@@ -112,6 +128,12 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
112
128
|
const encoder = new TextEncoder();
|
|
113
129
|
const buffer = encoder.encode(content).buffer;
|
|
114
130
|
const contentKey = generateContentKey(buffer);
|
|
131
|
+
|
|
132
|
+
// Release previous blob URL if content changed
|
|
133
|
+
if (contentKeyRef.current && contentKeyRef.current !== contentKey) {
|
|
134
|
+
releaseBlobUrl(contentKeyRef.current);
|
|
135
|
+
}
|
|
136
|
+
|
|
115
137
|
contentKeyRef.current = contentKey;
|
|
116
138
|
const url = getOrCreateBlobUrl(contentKey, buffer, mimeType || 'image/png');
|
|
117
139
|
imageDebug.load(url, 'blob');
|
|
@@ -122,19 +144,21 @@ export function useImageLoading(options: UseImageLoadingOptions): UseImageLoadin
|
|
|
122
144
|
|
|
123
145
|
// Handle ArrayBuffer with cached blob URL
|
|
124
146
|
const contentKey = generateContentKey(content);
|
|
147
|
+
|
|
148
|
+
// Release previous blob URL if content changed
|
|
149
|
+
if (contentKeyRef.current && contentKeyRef.current !== contentKey) {
|
|
150
|
+
releaseBlobUrl(contentKeyRef.current);
|
|
151
|
+
}
|
|
152
|
+
|
|
125
153
|
contentKeyRef.current = contentKey;
|
|
126
154
|
const url = getOrCreateBlobUrl(contentKey, content, mimeType || 'image/png');
|
|
127
155
|
imageDebug.load(url, 'blob');
|
|
128
156
|
imageDebug.state('loaded', { size, mimeType, contentKey });
|
|
129
157
|
setSrc(url);
|
|
130
158
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
contentKeyRef.current = null;
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
}, [content, mimeType, hasContent, size, directSrc, getOrCreateBlobUrl, releaseBlobUrl]);
|
|
159
|
+
// No cleanup here - cleanup happens in unmount effect above
|
|
160
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
161
|
+
}, [content, mimeType, hasContent, size, directSrc]);
|
|
138
162
|
|
|
139
163
|
// Create LQIP for progressive loading
|
|
140
164
|
useEffect(() => {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { useRef, useEffect, useCallback } from 'react';
|
|
10
|
-
import {
|
|
10
|
+
import { useMediaCacheStore } from '../../../stores/mediaCache';
|
|
11
11
|
|
|
12
12
|
// =============================================================================
|
|
13
13
|
// TYPES
|
|
@@ -54,7 +54,10 @@ export function useVideoPositionCache(
|
|
|
54
54
|
): UseVideoPositionCacheReturn {
|
|
55
55
|
const { cacheKey, currentTime, duration, isPlaying, isReady, onSeek } = options;
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
// Get stable function references from store
|
|
58
|
+
const saveVideoPosition = useMediaCacheStore.getState().saveVideoPosition;
|
|
59
|
+
const getVideoPosition = useMediaCacheStore.getState().getVideoPosition;
|
|
60
|
+
|
|
58
61
|
const lastSavedTimeRef = useRef<number>(0);
|
|
59
62
|
const hasRestoredRef = useRef<boolean>(false);
|
|
60
63
|
|
|
@@ -70,7 +73,8 @@ export function useVideoPositionCache(
|
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
hasRestoredRef.current = true;
|
|
73
|
-
|
|
76
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
+
}, [isReady, cacheKey, duration, onSeek]);
|
|
74
78
|
|
|
75
79
|
// Reset restored flag when cache key changes
|
|
76
80
|
useEffect(() => {
|
|
@@ -87,20 +91,23 @@ export function useVideoPositionCache(
|
|
|
87
91
|
saveVideoPosition(cacheKey, currentTime);
|
|
88
92
|
lastSavedTimeRef.current = currentTime;
|
|
89
93
|
}
|
|
90
|
-
|
|
94
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
95
|
+
}, [cacheKey, isPlaying, currentTime]);
|
|
91
96
|
|
|
92
97
|
const savePosition = useCallback(() => {
|
|
93
98
|
if (cacheKey && currentTime > 0) {
|
|
94
99
|
saveVideoPosition(cacheKey, currentTime);
|
|
95
100
|
lastSavedTimeRef.current = currentTime;
|
|
96
101
|
}
|
|
97
|
-
|
|
102
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
103
|
+
}, [cacheKey, currentTime]);
|
|
98
104
|
|
|
99
105
|
const clearPosition = useCallback(() => {
|
|
100
106
|
if (cacheKey) {
|
|
101
107
|
saveVideoPosition(cacheKey, 0);
|
|
102
108
|
}
|
|
103
|
-
|
|
109
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
110
|
+
}, [cacheKey]);
|
|
104
111
|
|
|
105
112
|
return {
|
|
106
113
|
savePosition,
|
|
@@ -14,7 +14,7 @@ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef,
|
|
|
14
14
|
|
|
15
15
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
16
16
|
import { Preloader, AspectRatio } from '@djangocfg/ui-core';
|
|
17
|
-
import {
|
|
17
|
+
import { useMediaCacheStore, generateContentKey } from '../../../stores/mediaCache';
|
|
18
18
|
|
|
19
19
|
import type { StreamProviderProps, VideoPlayerRef, StreamSource, BlobSource, DataUrlSource, ErrorFallbackProps } from '../types';
|
|
20
20
|
import { videoDebug } from '../utils/debug';
|
|
@@ -77,14 +77,12 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
77
77
|
const contentKeyRef = useRef<string | null>(null);
|
|
78
78
|
const lastSavedTimeRef = useRef<number>(0);
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
getVideoPosition,
|
|
87
|
-
} = useVideoCache();
|
|
80
|
+
// Get stable function references from store (not from hook to avoid re-renders)
|
|
81
|
+
const getOrCreateBlobUrl = useMediaCacheStore.getState().getOrCreateBlobUrl;
|
|
82
|
+
const releaseBlobUrl = useMediaCacheStore.getState().releaseBlobUrl;
|
|
83
|
+
const getOrCreateStreamUrl = useMediaCacheStore.getState().getOrCreateStreamUrl;
|
|
84
|
+
const saveVideoPosition = useMediaCacheStore.getState().saveVideoPosition;
|
|
85
|
+
const getVideoPosition = useMediaCacheStore.getState().getVideoPosition;
|
|
88
86
|
|
|
89
87
|
// Retry function for error fallback
|
|
90
88
|
// Regenerates URL for stream sources to get fresh token/session
|
|
@@ -143,12 +141,31 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
143
141
|
[]
|
|
144
142
|
);
|
|
145
143
|
|
|
144
|
+
// Track unmount for cleanup
|
|
145
|
+
const isMountedRef = useRef(true);
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
isMountedRef.current = true;
|
|
148
|
+
return () => {
|
|
149
|
+
isMountedRef.current = false;
|
|
150
|
+
// Release blob URL only on actual unmount
|
|
151
|
+
if (contentKeyRef.current) {
|
|
152
|
+
useMediaCacheStore.getState().releaseBlobUrl(contentKeyRef.current);
|
|
153
|
+
contentKeyRef.current = null;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
146
158
|
// Create video URL based on source type with caching
|
|
147
159
|
useEffect(() => {
|
|
148
|
-
//
|
|
160
|
+
// Release previous blob URL if source changed
|
|
149
161
|
if (contentKeyRef.current) {
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
const newKey = source.type === 'blob'
|
|
163
|
+
? generateContentKey((source as BlobSource).data)
|
|
164
|
+
: null;
|
|
165
|
+
if (newKey !== contentKeyRef.current) {
|
|
166
|
+
releaseBlobUrl(contentKeyRef.current);
|
|
167
|
+
contentKeyRef.current = null;
|
|
168
|
+
}
|
|
152
169
|
}
|
|
153
170
|
|
|
154
171
|
setHasError(false);
|
|
@@ -198,13 +215,9 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
198
215
|
setErrorMessage('Invalid video source');
|
|
199
216
|
}
|
|
200
217
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
contentKeyRef.current = null;
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
}, [source, getOrCreateBlobUrl, getOrCreateStreamUrl, releaseBlobUrl]);
|
|
218
|
+
// No cleanup here - cleanup happens in unmount effect above
|
|
219
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
220
|
+
}, [source]);
|
|
208
221
|
|
|
209
222
|
// Get source key for position caching
|
|
210
223
|
const getSourceKey = useCallback(() => {
|
|
@@ -244,7 +257,8 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
244
257
|
}
|
|
245
258
|
|
|
246
259
|
onCanPlay?.();
|
|
247
|
-
|
|
260
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
261
|
+
}, [getSourceKey, onCanPlay]);
|
|
248
262
|
|
|
249
263
|
// Save playback position periodically
|
|
250
264
|
const handleTimeUpdate = useCallback(() => {
|
|
@@ -262,7 +276,8 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
262
276
|
}
|
|
263
277
|
|
|
264
278
|
onTimeUpdate?.(video.currentTime, video.duration);
|
|
265
|
-
|
|
279
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
280
|
+
}, [getSourceKey, onTimeUpdate]);
|
|
266
281
|
|
|
267
282
|
// Save position on pause
|
|
268
283
|
const handlePause = useCallback(() => {
|
|
@@ -273,7 +288,8 @@ export const StreamProvider = forwardRef<VideoPlayerRef, StreamProviderProps>(
|
|
|
273
288
|
lastSavedTimeRef.current = video.currentTime;
|
|
274
289
|
}
|
|
275
290
|
onPause?.();
|
|
276
|
-
|
|
291
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
292
|
+
}, [getSourceKey, onPause]);
|
|
277
293
|
|
|
278
294
|
// Handle buffer progress
|
|
279
295
|
const handleProgress = useCallback(() => {
|