@djangocfg/ui-nextjs 2.1.81 → 2.1.83

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 (36) hide show
  1. package/package.json +4 -4
  2. package/src/tools/AudioPlayer/@refactoring3/00-IMPLEMENTATION-ROADMAP.md +1146 -0
  3. package/src/tools/AudioPlayer/@refactoring3/01-WAVESURFER-STREAMING-ANALYSIS.md +611 -0
  4. package/src/tools/AudioPlayer/@refactoring3/02-MEDIA-VIEWER-ANALYSIS.md +560 -0
  5. package/src/tools/AudioPlayer/@refactoring3/03-HYBRID-ARCHITECTURE-PROPOSAL.md +769 -0
  6. package/src/tools/AudioPlayer/@refactoring3/04-CRACKLING-ISSUE-DIAGNOSIS.md +373 -0
  7. package/src/tools/AudioPlayer/README.md +177 -205
  8. package/src/tools/AudioPlayer/components/AudioPlayer.tsx +9 -4
  9. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +251 -0
  10. package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +291 -0
  11. package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
  12. package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +16 -26
  13. package/src/tools/AudioPlayer/components/index.ts +6 -1
  14. package/src/tools/AudioPlayer/context/AudioProvider.tsx +16 -8
  15. package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +121 -0
  16. package/src/tools/AudioPlayer/context/index.ts +14 -2
  17. package/src/tools/AudioPlayer/hooks/index.ts +11 -0
  18. package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
  19. package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
  20. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +6 -3
  21. package/src/tools/AudioPlayer/index.ts +31 -0
  22. package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +8 -0
  23. package/src/tools/ImageViewer/hooks/useImageLoading.ts +33 -9
  24. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +13 -6
  25. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +38 -22
  26. package/src/tools/index.ts +22 -0
  27. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +0 -148
  28. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +0 -301
  29. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +0 -281
  30. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +0 -328
  31. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +0 -251
  32. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +0 -427
  33. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +0 -193
  34. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +0 -146
  35. package/src/tools/AudioPlayer/@refactoring2/ISSUE_ANALYSIS.md +0 -187
  36. package/src/tools/AudioPlayer/@refactoring2/PLAN.md +0 -372
@@ -1,281 +0,0 @@
1
- # Phase 3: Hooks Extraction
2
-
3
- ## Source: `context.tsx` (lines 25-229)
4
-
5
- Extract two internal hooks from context.tsx.
6
-
7
- ---
8
-
9
- ## `hooks/useSharedWebAudio.ts`
10
-
11
- Web Audio API context management. Prevents "InvalidStateError" from multiple MediaElementSourceNodes.
12
-
13
- ```typescript
14
- 'use client';
15
-
16
- import { useRef, useEffect, useCallback } from 'react';
17
- import type { SharedWebAudioContext } from '../types';
18
-
19
- /**
20
- * Manages a shared Web Audio context and source node.
21
- * All analyzers share the same source to prevent InvalidStateError.
22
- */
23
- export function useSharedWebAudio(audioElement: HTMLMediaElement | null): SharedWebAudioContext {
24
- const audioContextRef = useRef<AudioContext | null>(null);
25
- const sourceRef = useRef<MediaElementAudioSourceNode | null>(null);
26
- const connectedElementRef = useRef<HTMLMediaElement | null>(null);
27
- const analyserNodesRef = useRef<Set<AnalyserNode>>(new Set());
28
-
29
- // Initialize Web Audio on first play
30
- useEffect(() => {
31
- if (!audioElement) return;
32
-
33
- if (connectedElementRef.current === audioElement && audioContextRef.current) {
34
- return;
35
- }
36
-
37
- const initAudio = () => {
38
- try {
39
- if (!audioContextRef.current) {
40
- const AudioContextClass = window.AudioContext ||
41
- (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
42
- audioContextRef.current = new AudioContextClass();
43
- }
44
-
45
- const audioContext = audioContextRef.current;
46
-
47
- if (connectedElementRef.current !== audioElement) {
48
- if (sourceRef.current) {
49
- try { sourceRef.current.disconnect(); } catch { /* ignore */ }
50
- }
51
-
52
- sourceRef.current = audioContext.createMediaElementSource(audioElement);
53
- sourceRef.current.connect(audioContext.destination);
54
- connectedElementRef.current = audioElement;
55
- }
56
- } catch (error) {
57
- console.warn('[SharedWebAudio] Could not initialize:', error);
58
- }
59
- };
60
-
61
- const handlePlay = () => {
62
- initAudio();
63
- if (audioContextRef.current?.state === 'suspended') {
64
- audioContextRef.current.resume();
65
- }
66
- };
67
-
68
- audioElement.addEventListener('play', handlePlay);
69
- if (!audioElement.paused) {
70
- handlePlay();
71
- }
72
-
73
- return () => {
74
- audioElement.removeEventListener('play', handlePlay);
75
- };
76
- }, [audioElement]);
77
-
78
- const createAnalyser = useCallback((options?: { fftSize?: number; smoothing?: number }): AnalyserNode | null => {
79
- if (!audioContextRef.current || !sourceRef.current) return null;
80
-
81
- try {
82
- const analyser = audioContextRef.current.createAnalyser();
83
- analyser.fftSize = options?.fftSize ?? 256;
84
- analyser.smoothingTimeConstant = options?.smoothing ?? 0.85;
85
-
86
- sourceRef.current.connect(analyser);
87
- analyser.connect(audioContextRef.current.destination);
88
-
89
- analyserNodesRef.current.add(analyser);
90
- return analyser;
91
- } catch (error) {
92
- console.warn('[SharedWebAudio] Could not create analyser:', error);
93
- return null;
94
- }
95
- }, []);
96
-
97
- const disconnectAnalyser = useCallback((analyser: AnalyserNode) => {
98
- try {
99
- analyser.disconnect();
100
- analyserNodesRef.current.delete(analyser);
101
- } catch { /* ignore */ }
102
- }, []);
103
-
104
- return {
105
- audioContext: audioContextRef.current,
106
- sourceNode: sourceRef.current,
107
- createAnalyser,
108
- disconnectAnalyser,
109
- };
110
- }
111
- ```
112
-
113
- ---
114
-
115
- ## `hooks/useAudioAnalysis.ts`
116
-
117
- Real-time audio frequency analysis for reactive effects.
118
-
119
- ```typescript
120
- 'use client';
121
-
122
- import { useState, useRef, useEffect, useCallback } from 'react';
123
- import type { SharedWebAudioContext, AudioLevels } from '../types';
124
-
125
- /**
126
- * Analyzes audio frequencies for bass, mid, high, and overall levels.
127
- * Uses shared Web Audio context to prevent duplicate source nodes.
128
- */
129
- export function useAudioAnalysis(
130
- sharedAudio: SharedWebAudioContext,
131
- isPlaying: boolean
132
- ): AudioLevels {
133
- const [levels, setLevels] = useState<AudioLevels>({ bass: 0, mid: 0, high: 0, overall: 0 });
134
- const analyserRef = useRef<AnalyserNode | null>(null);
135
- const animationRef = useRef<number | null>(null);
136
- const dataArrayRef = useRef<Uint8Array | null>(null);
137
-
138
- const cleanup = useCallback(() => {
139
- if (animationRef.current) {
140
- cancelAnimationFrame(animationRef.current);
141
- animationRef.current = null;
142
- }
143
- }, []);
144
-
145
- // Create analyser when shared audio is ready
146
- useEffect(() => {
147
- if (!sharedAudio.sourceNode || analyserRef.current) return;
148
-
149
- const analyser = sharedAudio.createAnalyser({ fftSize: 256, smoothing: 0.85 });
150
- if (analyser) {
151
- analyserRef.current = analyser;
152
- dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount);
153
- }
154
-
155
- return () => {
156
- if (analyserRef.current) {
157
- sharedAudio.disconnectAnalyser(analyserRef.current);
158
- analyserRef.current = null;
159
- dataArrayRef.current = null;
160
- }
161
- };
162
- }, [sharedAudio.sourceNode, sharedAudio.createAnalyser, sharedAudio.disconnectAnalyser]);
163
-
164
- // Animation loop
165
- useEffect(() => {
166
- if (!isPlaying || !analyserRef.current || !dataArrayRef.current) {
167
- cleanup();
168
- // Smooth fade out
169
- setLevels(prev => ({
170
- bass: prev.bass * 0.95 < 0.01 ? 0 : prev.bass * 0.95,
171
- mid: prev.mid * 0.95,
172
- high: prev.high * 0.95,
173
- overall: prev.overall * 0.95,
174
- }));
175
- return;
176
- }
177
-
178
- const analyser = analyserRef.current;
179
- const dataArray = dataArrayRef.current;
180
-
181
- const animate = () => {
182
- analyser.getByteFrequencyData(dataArray);
183
- const binCount = dataArray.length;
184
-
185
- // Bass (0-15%)
186
- const bassEnd = Math.floor(binCount * 0.15);
187
- let bassSum = 0;
188
- for (let i = 0; i < bassEnd; i++) bassSum += dataArray[i];
189
- const bass = bassSum / bassEnd / 255;
190
-
191
- // Mids (15-50%)
192
- const midStart = bassEnd;
193
- const midEnd = Math.floor(binCount * 0.5);
194
- let midSum = 0;
195
- for (let i = midStart; i < midEnd; i++) midSum += dataArray[i];
196
- const mid = midSum / (midEnd - midStart) / 255;
197
-
198
- // Highs (50-100%)
199
- const highStart = midEnd;
200
- let highSum = 0;
201
- for (let i = highStart; i < binCount; i++) highSum += dataArray[i];
202
- const high = highSum / (binCount - highStart) / 255;
203
-
204
- // Overall
205
- let totalSum = 0;
206
- for (let i = 0; i < binCount; i++) totalSum += dataArray[i];
207
- const overall = totalSum / binCount / 255;
208
-
209
- // Smooth with lerp
210
- setLevels(prev => ({
211
- bass: prev.bass * 0.7 + bass * 0.3,
212
- mid: prev.mid * 0.7 + mid * 0.3,
213
- high: prev.high * 0.7 + high * 0.3,
214
- overall: prev.overall * 0.7 + overall * 0.3,
215
- }));
216
-
217
- animationRef.current = requestAnimationFrame(animate);
218
- };
219
-
220
- animationRef.current = requestAnimationFrame(animate);
221
- return cleanup;
222
- }, [isPlaying, cleanup]);
223
-
224
- return levels;
225
- }
226
- ```
227
-
228
- ---
229
-
230
- ## `hooks/useAudioHotkeys.ts`
231
-
232
- Move existing file, update imports.
233
-
234
- ```typescript
235
- // Same content, just update imports:
236
- // - import type { ... } from '../types';
237
- // - import { useAudioControls, useAudioState } from '../context';
238
- ```
239
-
240
- ---
241
-
242
- ## `hooks/useVisualization.tsx`
243
-
244
- Rename from `useAudioVisualization.tsx`, update imports.
245
-
246
- ```typescript
247
- // Same content, just update imports
248
- // Export both old and new names for backward compatibility
249
- export { useVisualization as useAudioVisualization };
250
- ```
251
-
252
- ---
253
-
254
- ## `hooks/index.ts`
255
-
256
- ```typescript
257
- // Internal hooks (used by provider)
258
- export { useSharedWebAudio } from './useSharedWebAudio';
259
- export { useAudioAnalysis } from './useAudioAnalysis';
260
-
261
- // Public hooks
262
- export { useAudioHotkeys, AUDIO_SHORTCUTS } from './useAudioHotkeys';
263
- export type { AudioHotkeyOptions, ShortcutItem, ShortcutGroup } from './useAudioHotkeys';
264
-
265
- export {
266
- useVisualization,
267
- useAudioVisualization, // backward compat alias
268
- VisualizationProvider,
269
- VARIANT_INFO,
270
- INTENSITY_INFO,
271
- COLOR_SCHEME_INFO,
272
- } from './useVisualization';
273
- export type {
274
- VisualizationSettings,
275
- VisualizationVariant,
276
- VisualizationIntensity,
277
- VisualizationColorScheme,
278
- UseAudioVisualizationReturn,
279
- VisualizationProviderProps,
280
- } from './useVisualization';
281
- ```
@@ -1,328 +0,0 @@
1
- # Phase 4: Context Refactoring
2
-
3
- ## Source: `context.tsx` (lines 231-574)
4
-
5
- Split provider from selector hooks.
6
-
7
- ---
8
-
9
- ## `context/AudioProvider.tsx`
10
-
11
- Provider component only (~150 lines).
12
-
13
- ```typescript
14
- 'use client';
15
-
16
- import {
17
- createContext,
18
- useRef,
19
- useMemo,
20
- useCallback,
21
- useState,
22
- useEffect,
23
- type ReactNode,
24
- } from 'react';
25
- import { useWavesurfer } from '@wavesurfer/react';
26
- import type { AudioContextState, AudioSource, WaveformOptions } from '../types';
27
- import { useSharedWebAudio, useAudioAnalysis } from '../hooks';
28
- import { useAudioCache } from '../../../stores/mediaCache';
29
-
30
- // =============================================================================
31
- // CONTEXT
32
- // =============================================================================
33
-
34
- export const AudioPlayerContext = createContext<AudioContextState | null>(null);
35
-
36
- // =============================================================================
37
- // PROVIDER PROPS
38
- // =============================================================================
39
-
40
- interface AudioProviderProps {
41
- source: AudioSource;
42
- autoPlay?: boolean;
43
- waveformOptions?: WaveformOptions;
44
- containerRef: React.RefObject<HTMLDivElement | null>;
45
- children: ReactNode;
46
- }
47
-
48
- // =============================================================================
49
- // PROVIDER
50
- // =============================================================================
51
-
52
- export function AudioProvider({
53
- source,
54
- autoPlay = false,
55
- waveformOptions = {},
56
- containerRef,
57
- children,
58
- }: AudioProviderProps) {
59
- // Cache for playback position persistence
60
- const { saveAudioPosition, getAudioPosition } = useAudioCache();
61
- const lastSavedTimeRef = useRef<number>(0);
62
-
63
- // Memoize WaveSurfer options
64
- const options = useMemo(
65
- () => ({
66
- container: containerRef,
67
- url: source.uri,
68
- waveColor: waveformOptions.waveColor || 'hsl(217 91% 60% / 0.3)',
69
- progressColor: waveformOptions.progressColor || 'hsl(217 91% 60%)',
70
- cursorColor: waveformOptions.cursorColor || 'hsl(217 91% 60%)',
71
- cursorWidth: waveformOptions.cursorWidth ?? 2,
72
- height: waveformOptions.height ?? 64,
73
- barWidth: waveformOptions.barWidth ?? 3,
74
- barRadius: waveformOptions.barRadius ?? 3,
75
- barGap: waveformOptions.barGap ?? 2,
76
- normalize: true,
77
- interact: true,
78
- hideScrollbar: true,
79
- autoplay: autoPlay,
80
- }),
81
- [source.uri, autoPlay, waveformOptions, containerRef]
82
- );
83
-
84
- const { wavesurfer, isReady, isPlaying, currentTime } = useWavesurfer(options);
85
-
86
- // Restore cached position
87
- useEffect(() => {
88
- if (isReady && wavesurfer && source.uri) {
89
- const cachedPosition = getAudioPosition(source.uri);
90
- if (cachedPosition && cachedPosition > 0) {
91
- const duration = wavesurfer.getDuration();
92
- if (cachedPosition < duration - 1) {
93
- wavesurfer.setTime(cachedPosition);
94
- }
95
- }
96
- }
97
- }, [isReady, wavesurfer, source.uri, getAudioPosition]);
98
-
99
- // Save position periodically
100
- useEffect(() => {
101
- if (!source.uri) return;
102
-
103
- if (isPlaying && currentTime > 0) {
104
- const timeSinceLastSave = currentTime - lastSavedTimeRef.current;
105
- if (timeSinceLastSave >= 5 || timeSinceLastSave < 0) {
106
- saveAudioPosition(source.uri, currentTime);
107
- lastSavedTimeRef.current = currentTime;
108
- }
109
- }
110
-
111
- if (!isPlaying && currentTime > 0) {
112
- saveAudioPosition(source.uri, currentTime);
113
- lastSavedTimeRef.current = currentTime;
114
- }
115
- }, [isPlaying, currentTime, source.uri, saveAudioPosition]);
116
-
117
- // Derived state
118
- const duration = wavesurfer?.getDuration() ?? 0;
119
- const volume = wavesurfer?.getVolume() ?? 1;
120
- const isMuted = wavesurfer?.getMuted() ?? false;
121
-
122
- // Loop state
123
- const [isLooping, setIsLooping] = useState(false);
124
-
125
- useEffect(() => {
126
- if (!wavesurfer) return;
127
-
128
- const handleFinish = () => {
129
- if (isLooping) {
130
- wavesurfer.seekTo(0);
131
- wavesurfer.play();
132
- }
133
- };
134
-
135
- wavesurfer.on('finish', handleFinish);
136
- return () => {
137
- wavesurfer.un('finish', handleFinish);
138
- };
139
- }, [wavesurfer, isLooping]);
140
-
141
- // Audio element
142
- const audioElement = useMemo(() => wavesurfer?.getMediaElement() ?? null, [wavesurfer]);
143
-
144
- // Shared Web Audio context
145
- const sharedAudio = useSharedWebAudio(audioElement);
146
-
147
- // Audio analysis
148
- const audioLevels = useAudioAnalysis(sharedAudio, isPlaying);
149
-
150
- // Actions
151
- const play = useCallback(async () => { await wavesurfer?.play(); }, [wavesurfer]);
152
- const pause = useCallback(() => { wavesurfer?.pause(); }, [wavesurfer]);
153
- const togglePlay = useCallback(() => { wavesurfer?.playPause(); }, [wavesurfer]);
154
-
155
- const seek = useCallback((time: number) => {
156
- if (wavesurfer) {
157
- wavesurfer.setTime(Math.max(0, Math.min(time, duration)));
158
- }
159
- }, [wavesurfer, duration]);
160
-
161
- const seekTo = useCallback((progress: number) => {
162
- if (wavesurfer) {
163
- wavesurfer.seekTo(Math.max(0, Math.min(progress, 1)));
164
- }
165
- }, [wavesurfer]);
166
-
167
- const skip = useCallback((seconds: number) => { wavesurfer?.skip(seconds); }, [wavesurfer]);
168
-
169
- const setVolume = useCallback((vol: number) => {
170
- if (wavesurfer) {
171
- wavesurfer.setVolume(Math.max(0, Math.min(vol, 1)));
172
- }
173
- }, [wavesurfer]);
174
-
175
- const toggleMute = useCallback(() => {
176
- if (wavesurfer) {
177
- wavesurfer.setMuted(!wavesurfer.getMuted());
178
- }
179
- }, [wavesurfer]);
180
-
181
- const restart = useCallback(() => {
182
- if (wavesurfer) {
183
- wavesurfer.seekTo(0);
184
- wavesurfer.play();
185
- }
186
- }, [wavesurfer]);
187
-
188
- const toggleLoop = useCallback(() => { setIsLooping(prev => !prev); }, []);
189
- const setLoop = useCallback((enabled: boolean) => { setIsLooping(enabled); }, []);
190
-
191
- // Context value
192
- const contextValue = useMemo<AudioContextState>(
193
- () => ({
194
- wavesurfer,
195
- audioElement,
196
- sharedAudio,
197
- isReady,
198
- isPlaying,
199
- currentTime,
200
- duration,
201
- volume,
202
- isMuted,
203
- isLooping,
204
- audioLevels,
205
- play,
206
- pause,
207
- togglePlay,
208
- seek,
209
- seekTo,
210
- skip,
211
- setVolume,
212
- toggleMute,
213
- toggleLoop,
214
- setLoop,
215
- restart,
216
- }),
217
- [
218
- wavesurfer, audioElement, sharedAudio,
219
- isReady, isPlaying, currentTime, duration, volume, isMuted, isLooping,
220
- audioLevels,
221
- play, pause, togglePlay, seek, seekTo, skip, setVolume, toggleMute, toggleLoop, setLoop, restart,
222
- ]
223
- );
224
-
225
- return (
226
- <AudioPlayerContext.Provider value={contextValue}>
227
- {children}
228
- </AudioPlayerContext.Provider>
229
- );
230
- }
231
- ```
232
-
233
- ---
234
-
235
- ## `context/selectors.ts`
236
-
237
- Selector hooks for performance optimization.
238
-
239
- ```typescript
240
- 'use client';
241
-
242
- import { useContext } from 'react';
243
- import { AudioPlayerContext } from './AudioProvider';
244
- import type { AudioContextState } from '../types';
245
-
246
- // =============================================================================
247
- // MAIN HOOK
248
- // =============================================================================
249
-
250
- export function useAudio(): AudioContextState {
251
- const context = useContext(AudioPlayerContext);
252
- if (!context) {
253
- throw new Error('useAudio must be used within an AudioProvider');
254
- }
255
- return context;
256
- }
257
-
258
- // =============================================================================
259
- // SELECTIVE HOOKS - for performance optimization
260
- // =============================================================================
261
-
262
- /** Hook for playback controls only (no re-render on time updates) */
263
- export function useAudioControls() {
264
- const {
265
- isReady,
266
- play,
267
- pause,
268
- togglePlay,
269
- skip,
270
- restart,
271
- setVolume,
272
- toggleMute,
273
- toggleLoop,
274
- setLoop,
275
- } = useAudio();
276
-
277
- return {
278
- isReady,
279
- play,
280
- pause,
281
- togglePlay,
282
- skip,
283
- restart,
284
- setVolume,
285
- toggleMute,
286
- toggleLoop,
287
- setLoop,
288
- };
289
- }
290
-
291
- /** Hook for playback state (read-only) */
292
- export function useAudioState() {
293
- const {
294
- isReady,
295
- isPlaying,
296
- currentTime,
297
- duration,
298
- volume,
299
- isMuted,
300
- isLooping,
301
- } = useAudio();
302
-
303
- return {
304
- isReady,
305
- isPlaying,
306
- currentTime,
307
- duration,
308
- volume,
309
- isMuted,
310
- isLooping,
311
- };
312
- }
313
-
314
- /** Hook for audio element access (for equalizer and reactive effects) */
315
- export function useAudioElement() {
316
- const { audioElement, sharedAudio, isPlaying, audioLevels } = useAudio();
317
- return { audioElement, sharedAudio, isPlaying, audioLevels };
318
- }
319
- ```
320
-
321
- ---
322
-
323
- ## `context/index.ts`
324
-
325
- ```typescript
326
- export { AudioPlayerContext, AudioProvider } from './AudioProvider';
327
- export { useAudio, useAudioControls, useAudioState, useAudioElement } from './selectors';
328
- ```