@djangocfg/ui-nextjs 2.1.82 → 2.1.84

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 (43) hide show
  1. package/package.json +4 -4
  2. package/src/tools/AudioPlayer/README.md +108 -242
  3. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +216 -0
  4. package/src/tools/AudioPlayer/components/{SimpleAudioPlayer.tsx → HybridSimplePlayer.tsx} +61 -69
  5. package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
  6. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +5 -5
  7. package/src/tools/AudioPlayer/components/index.ts +7 -6
  8. package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +121 -0
  9. package/src/tools/AudioPlayer/context/index.ts +11 -6
  10. package/src/tools/AudioPlayer/hooks/index.ts +14 -10
  11. package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
  12. package/src/tools/AudioPlayer/hooks/{useAudioAnalysis.ts → useHybridAudioAnalysis.ts} +23 -38
  13. package/src/tools/AudioPlayer/index.ts +37 -70
  14. package/src/tools/AudioPlayer/types/index.ts +10 -18
  15. package/src/tools/index.ts +60 -43
  16. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +0 -148
  17. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +0 -301
  18. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +0 -281
  19. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +0 -328
  20. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +0 -251
  21. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +0 -427
  22. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +0 -193
  23. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +0 -146
  24. package/src/tools/AudioPlayer/@refactoring2/ISSUE_ANALYSIS.md +0 -187
  25. package/src/tools/AudioPlayer/@refactoring2/PLAN.md +0 -372
  26. package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +0 -200
  27. package/src/tools/AudioPlayer/components/AudioPlayer.tsx +0 -231
  28. package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +0 -99
  29. package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +0 -64
  30. package/src/tools/AudioPlayer/context/AudioProvider.tsx +0 -371
  31. package/src/tools/AudioPlayer/context/selectors.ts +0 -96
  32. package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +0 -150
  33. package/src/tools/AudioPlayer/hooks/useAudioSource.ts +0 -155
  34. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +0 -106
  35. package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +0 -295
  36. package/src/tools/AudioPlayer/progressive/WaveformCanvas.tsx +0 -381
  37. package/src/tools/AudioPlayer/progressive/index.ts +0 -40
  38. package/src/tools/AudioPlayer/progressive/peaks.ts +0 -234
  39. package/src/tools/AudioPlayer/progressive/types.ts +0 -179
  40. package/src/tools/AudioPlayer/progressive/useAudioElement.ts +0 -340
  41. package/src/tools/AudioPlayer/progressive/useProgressiveWaveform.ts +0 -267
  42. package/src/tools/AudioPlayer/types/audio.ts +0 -121
  43. package/src/tools/AudioPlayer/types/components.ts +0 -98
@@ -1,155 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * useAudioSource - Handles audio source loading with optional prefetch
5
- *
6
- * For streaming URLs, WaveSurfer needs the complete file to enable seeking.
7
- * This hook fetches the URL as blob when prefetch is enabled.
8
- */
9
-
10
- import { useState, useEffect, useRef } from 'react';
11
- import type { AudioSource } from '../types';
12
- import { audioDebug } from '../utils/debug';
13
-
14
- export interface UseAudioSourceResult {
15
- /** The resolved URL (blob URL if prefetched, original URL otherwise) */
16
- url: string | null;
17
- /** Whether the source is currently being fetched */
18
- isLoading: boolean;
19
- /** Error message if fetch failed */
20
- error: string | null;
21
- /** Progress percentage (0-100) during fetch */
22
- progress: number;
23
- }
24
-
25
- export function useAudioSource(source: AudioSource): UseAudioSourceResult {
26
- const [url, setUrl] = useState<string | null>(null);
27
- const [isLoading, setIsLoading] = useState(false);
28
- const [error, setError] = useState<string | null>(null);
29
- const [progress, setProgress] = useState(0);
30
- const blobUrlRef = useRef<string | null>(null);
31
-
32
- useEffect(() => {
33
- // Cleanup previous blob URL
34
- if (blobUrlRef.current) {
35
- URL.revokeObjectURL(blobUrlRef.current);
36
- blobUrlRef.current = null;
37
- }
38
-
39
- // Reset state
40
- setError(null);
41
- setProgress(0);
42
-
43
- // No prefetch - use URL directly
44
- if (!source.prefetch) {
45
- audioDebug.debug('Using direct URL (prefetch disabled)', { uri: source.uri });
46
- setUrl(source.uri);
47
- setIsLoading(false);
48
- return;
49
- }
50
-
51
- // Prefetch enabled - fetch as blob
52
- const abortController = new AbortController();
53
- setIsLoading(true);
54
-
55
- const fetchAsBlob = async () => {
56
- try {
57
- audioDebug.info('Prefetching audio as blob', { uri: source.uri });
58
-
59
- const response = await fetch(source.uri, {
60
- signal: abortController.signal,
61
- headers: {
62
- // Request full file - some servers require Range header
63
- 'Range': 'bytes=0-',
64
- },
65
- });
66
-
67
- // Accept 200 OK or 206 Partial Content (response.ok covers both)
68
- if (!response.ok) {
69
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
70
- }
71
-
72
- // Get content length for progress tracking
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
- }
85
-
86
- if (!response.body) {
87
- // Fallback for browsers without ReadableStream
88
- const blob = await response.blob();
89
- const blobUrl = URL.createObjectURL(blob);
90
- blobUrlRef.current = blobUrl;
91
- setUrl(blobUrl);
92
- setProgress(100);
93
- audioDebug.success('Audio prefetched (no stream)', { size: blob.size });
94
- return;
95
- }
96
-
97
- // Stream the response for progress tracking
98
- const reader = response.body.getReader();
99
- const chunks: ArrayBuffer[] = [];
100
- let receivedBytes = 0;
101
-
102
- while (true) {
103
- const { done, value } = await reader.read();
104
-
105
- if (done) break;
106
-
107
- // Convert Uint8Array to ArrayBuffer for Blob compatibility
108
- chunks.push(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
109
- receivedBytes += value.length;
110
-
111
- if (totalBytes > 0) {
112
- setProgress(Math.round((receivedBytes / totalBytes) * 100));
113
- }
114
- }
115
-
116
- // Combine chunks into blob
117
- const blob = new Blob(chunks, { type: 'audio/mpeg' });
118
- const blobUrl = URL.createObjectURL(blob);
119
- blobUrlRef.current = blobUrl;
120
- setUrl(blobUrl);
121
- setProgress(100);
122
-
123
- audioDebug.success('Audio prefetched', {
124
- size: blob.size,
125
- sizeFormatted: `${(blob.size / 1024 / 1024).toFixed(2)} MB`,
126
- });
127
- } catch (err) {
128
- if (err instanceof Error && err.name === 'AbortError') {
129
- return; // Ignore abort errors
130
- }
131
-
132
- const errorMessage = err instanceof Error ? err.message : 'Failed to prefetch audio';
133
- audioDebug.error('Failed to prefetch audio', { error: errorMessage, uri: source.uri });
134
- setError(errorMessage);
135
-
136
- // Fallback to direct URL (may have seek issues)
137
- setUrl(source.uri);
138
- } finally {
139
- setIsLoading(false);
140
- }
141
- };
142
-
143
- fetchAsBlob();
144
-
145
- return () => {
146
- abortController.abort();
147
- if (blobUrlRef.current) {
148
- URL.revokeObjectURL(blobUrlRef.current);
149
- blobUrlRef.current = null;
150
- }
151
- };
152
- }, [source.uri, source.prefetch]);
153
-
154
- return { url, isLoading, error, progress };
155
- }
@@ -1,106 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * useSharedWebAudio - Manages a shared Web Audio context and source node.
5
- *
6
- * This prevents the "InvalidStateError" from creating multiple MediaElementSourceNodes
7
- * for the same audio element. All analyzers share the same source.
8
- */
9
-
10
- import { useRef, useEffect, useCallback } from 'react';
11
- import type { SharedWebAudioContext } from '../types';
12
-
13
- export function useSharedWebAudio(audioElement: HTMLMediaElement | null): SharedWebAudioContext {
14
- const audioContextRef = useRef<AudioContext | null>(null);
15
- const sourceRef = useRef<MediaElementAudioSourceNode | null>(null);
16
- const connectedElementRef = useRef<HTMLMediaElement | null>(null);
17
- const analyserNodesRef = useRef<Set<AnalyserNode>>(new Set());
18
-
19
- // Initialize Web Audio on first play
20
- useEffect(() => {
21
- if (!audioElement) return;
22
-
23
- // Already connected to this element
24
- if (connectedElementRef.current === audioElement && audioContextRef.current) {
25
- return;
26
- }
27
-
28
- const initAudio = () => {
29
- try {
30
- if (!audioContextRef.current) {
31
- const AudioContextClass = window.AudioContext ||
32
- (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
33
- audioContextRef.current = new AudioContextClass();
34
- }
35
-
36
- const audioContext = audioContextRef.current;
37
-
38
- // Only create source node once per audio element
39
- if (connectedElementRef.current !== audioElement) {
40
- if (sourceRef.current) {
41
- try { sourceRef.current.disconnect(); } catch { /* ignore */ }
42
- }
43
-
44
- sourceRef.current = audioContext.createMediaElementSource(audioElement);
45
- // Connect directly to destination (analysers will be inserted in between)
46
- sourceRef.current.connect(audioContext.destination);
47
- connectedElementRef.current = audioElement;
48
- }
49
- } catch (error) {
50
- console.warn('[SharedWebAudio] Could not initialize:', error);
51
- }
52
- };
53
-
54
- const handlePlay = () => {
55
- initAudio();
56
- if (audioContextRef.current?.state === 'suspended') {
57
- audioContextRef.current.resume();
58
- }
59
- };
60
-
61
- audioElement.addEventListener('play', handlePlay);
62
- if (!audioElement.paused) {
63
- handlePlay();
64
- }
65
-
66
- return () => {
67
- audioElement.removeEventListener('play', handlePlay);
68
- };
69
- }, [audioElement]);
70
-
71
- // Create an analyser connected to the shared source
72
- const createAnalyser = useCallback((options?: { fftSize?: number; smoothing?: number }): AnalyserNode | null => {
73
- if (!audioContextRef.current || !sourceRef.current) return null;
74
-
75
- try {
76
- const analyser = audioContextRef.current.createAnalyser();
77
- analyser.fftSize = options?.fftSize ?? 256;
78
- analyser.smoothingTimeConstant = options?.smoothing ?? 0.85;
79
-
80
- // Connect: source -> analyser -> destination
81
- sourceRef.current.connect(analyser);
82
- analyser.connect(audioContextRef.current.destination);
83
-
84
- analyserNodesRef.current.add(analyser);
85
- return analyser;
86
- } catch (error) {
87
- console.warn('[SharedWebAudio] Could not create analyser:', error);
88
- return null;
89
- }
90
- }, []);
91
-
92
- // Disconnect an analyser
93
- const disconnectAnalyser = useCallback((analyser: AnalyserNode) => {
94
- try {
95
- analyser.disconnect();
96
- analyserNodesRef.current.delete(analyser);
97
- } catch { /* ignore */ }
98
- }, []);
99
-
100
- return {
101
- audioContext: audioContextRef.current,
102
- sourceNode: sourceRef.current,
103
- createAnalyser,
104
- disconnectAnalyser,
105
- };
106
- }
@@ -1,295 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * ProgressiveAudioPlayer - Audio player with progressive waveform loading
5
- *
6
- * Uses native HTML5 audio for playback (supports Range requests)
7
- * and custom canvas for waveform visualization.
8
- *
9
- * Features:
10
- * - Progressive waveform loading (no need to load entire file)
11
- * - Smooth seek on any position
12
- * - Buffered ranges visualization
13
- * - Keyboard shortcuts
14
- * - Volume control
15
- * - Loop mode
16
- */
17
-
18
- import { useRef, useCallback } from 'react';
19
- import {
20
- Play,
21
- Pause,
22
- RotateCcw,
23
- SkipBack,
24
- SkipForward,
25
- Volume2,
26
- VolumeX,
27
- Loader2,
28
- Repeat,
29
- AlertCircle,
30
- } from 'lucide-react';
31
- import { cn, Button, Slider } from '@djangocfg/ui-core';
32
-
33
- import { useAudioElement } from './useAudioElement';
34
- import { useProgressiveWaveform } from './useProgressiveWaveform';
35
- import { WaveformCanvas } from './WaveformCanvas';
36
- import { formatTime } from '../utils';
37
- import type { ProgressiveAudioPlayerProps, WaveformStyle } from './types';
38
-
39
- // =============================================================================
40
- // COMPONENT
41
- // =============================================================================
42
-
43
- export function ProgressiveAudioPlayer({
44
- src,
45
- title,
46
- artist,
47
- coverArt,
48
- showWaveform = true,
49
- showControls = true,
50
- showTimer = true,
51
- showVolume = true,
52
- showLoop = true,
53
- autoPlay = false,
54
- waveformStyle,
55
- className,
56
- onPlay,
57
- onPause,
58
- onEnded,
59
- onTimeUpdate,
60
- onError,
61
- }: ProgressiveAudioPlayerProps) {
62
- const audioRef = useRef<HTMLAudioElement>(null);
63
-
64
- // Audio element hook
65
- const audio = useAudioElement({
66
- src,
67
- autoPlay,
68
- onPlay,
69
- onPause,
70
- onEnded,
71
- onTimeUpdate,
72
- onError,
73
- });
74
-
75
- // Progressive waveform loading
76
- const waveform = useProgressiveWaveform({
77
- url: src,
78
- duration: audio.duration,
79
- enabled: showWaveform,
80
- });
81
-
82
- // Handlers
83
- const handleSeek = useCallback((time: number) => {
84
- audio.seek(time);
85
- }, [audio]);
86
-
87
- const handleVolumeChange = useCallback((value: number[]) => {
88
- audio.setVolume(value[0] / 100);
89
- }, [audio]);
90
-
91
- // Loading state
92
- const isLoading = !audio.isReady;
93
- const hasError = audio.error || waveform.error;
94
-
95
- // Default waveform style
96
- const defaultStyle: WaveformStyle = {
97
- waveColor: 'hsl(217 91% 60% / 0.3)',
98
- progressColor: 'hsl(217 91% 60%)',
99
- loadingColor: 'hsl(217 91% 60% / 0.1)',
100
- cursorColor: 'hsl(217 91% 60%)',
101
- barWidth: 3,
102
- barGap: 2,
103
- barRadius: 2,
104
- height: 64,
105
- ...waveformStyle,
106
- };
107
-
108
- return (
109
- <div className={cn('flex flex-col gap-3 p-4 rounded-lg bg-card border', className)}>
110
- {/* Hidden audio element */}
111
- <audio
112
- ref={(el) => {
113
- (audioRef as any).current = el;
114
- (audio.audioRef as any).current = el;
115
- }}
116
- preload="metadata"
117
- crossOrigin="anonymous"
118
- />
119
-
120
- {/* Header with title/artist */}
121
- {(title || artist) && (
122
- <div className="text-center">
123
- {title && <h3 className="text-sm font-medium truncate">{title}</h3>}
124
- {artist && <p className="text-xs text-muted-foreground truncate">{artist}</p>}
125
- </div>
126
- )}
127
-
128
- {/* Waveform */}
129
- {showWaveform && (
130
- <div className="relative">
131
- <WaveformCanvas
132
- peaks={waveform.peaks}
133
- currentTime={audio.currentTime}
134
- duration={audio.duration}
135
- buffered={audio.buffered}
136
- loadingPercent={waveform.loadedPercent}
137
- onSeek={handleSeek}
138
- interactive={audio.isReady}
139
- style={defaultStyle}
140
- className={cn(
141
- 'rounded transition-opacity',
142
- isLoading && 'opacity-50'
143
- )}
144
- />
145
-
146
- {/* Loading overlay */}
147
- {isLoading && (
148
- <div className="absolute inset-0 flex items-center justify-center">
149
- <Loader2 className="h-6 w-6 animate-spin text-primary" />
150
- </div>
151
- )}
152
-
153
- {/* Error overlay */}
154
- {hasError && (
155
- <div className="absolute inset-0 flex items-center justify-center bg-destructive/10 rounded">
156
- <div className="flex items-center gap-2 text-destructive text-sm">
157
- <AlertCircle className="h-4 w-4" />
158
- <span>Failed to load</span>
159
- <Button
160
- variant="ghost"
161
- size="sm"
162
- onClick={waveform.retry}
163
- className="h-6 px-2"
164
- >
165
- Retry
166
- </Button>
167
- </div>
168
- </div>
169
- )}
170
-
171
- {/* Loading progress indicator */}
172
- {waveform.isLoading && !isLoading && (
173
- <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-muted overflow-hidden rounded-full">
174
- <div
175
- className="h-full bg-primary transition-all duration-300"
176
- style={{ width: `${waveform.loadedPercent}%` }}
177
- />
178
- </div>
179
- )}
180
- </div>
181
- )}
182
-
183
- {/* Timer */}
184
- {showTimer && (
185
- <div className="flex justify-between text-xs text-muted-foreground tabular-nums px-1">
186
- <span>{formatTime(audio.currentTime)}</span>
187
- <span>{formatTime(audio.duration)}</span>
188
- </div>
189
- )}
190
-
191
- {/* Controls */}
192
- {showControls && (
193
- <div className="flex items-center justify-center gap-1">
194
- {/* Restart */}
195
- <Button
196
- variant="ghost"
197
- size="icon"
198
- className="h-9 w-9"
199
- onClick={() => audio.seek(0)}
200
- disabled={!audio.isReady}
201
- title="Restart"
202
- >
203
- <RotateCcw className="h-4 w-4" />
204
- </Button>
205
-
206
- {/* Skip back 5s */}
207
- <Button
208
- variant="ghost"
209
- size="icon"
210
- className="h-9 w-9"
211
- onClick={() => audio.skip(-5)}
212
- disabled={!audio.isReady}
213
- title="Back 5 seconds"
214
- >
215
- <SkipBack className="h-4 w-4" />
216
- </Button>
217
-
218
- {/* Play/Pause */}
219
- <Button
220
- variant="default"
221
- size="icon"
222
- className="h-12 w-12 rounded-full"
223
- onClick={audio.togglePlay}
224
- disabled={!audio.isReady && !isLoading}
225
- title={audio.isPlaying ? 'Pause' : 'Play'}
226
- >
227
- {isLoading ? (
228
- <Loader2 className="h-5 w-5 animate-spin" />
229
- ) : audio.isPlaying ? (
230
- <Pause className="h-5 w-5" />
231
- ) : (
232
- <Play className="h-5 w-5 ml-0.5" />
233
- )}
234
- </Button>
235
-
236
- {/* Skip forward 5s */}
237
- <Button
238
- variant="ghost"
239
- size="icon"
240
- className="h-9 w-9"
241
- onClick={() => audio.skip(5)}
242
- disabled={!audio.isReady}
243
- title="Forward 5 seconds"
244
- >
245
- <SkipForward className="h-4 w-4" />
246
- </Button>
247
-
248
- {/* Volume */}
249
- {showVolume && (
250
- <>
251
- <Button
252
- variant="ghost"
253
- size="icon"
254
- className="h-9 w-9"
255
- onClick={audio.toggleMute}
256
- title={audio.isMuted ? 'Unmute' : 'Mute'}
257
- >
258
- {audio.isMuted || audio.volume === 0 ? (
259
- <VolumeX className="h-4 w-4" />
260
- ) : (
261
- <Volume2 className="h-4 w-4" />
262
- )}
263
- </Button>
264
-
265
- <Slider
266
- value={[audio.isMuted ? 0 : audio.volume * 100]}
267
- max={100}
268
- step={1}
269
- onValueChange={handleVolumeChange}
270
- className="w-20"
271
- aria-label="Volume"
272
- />
273
- </>
274
- )}
275
-
276
- {/* Loop/Repeat */}
277
- {showLoop && (
278
- <Button
279
- variant="ghost"
280
- size="icon"
281
- className={cn('h-9 w-9', audio.isLooping && 'text-primary')}
282
- onClick={audio.toggleLoop}
283
- disabled={!audio.isReady}
284
- title={audio.isLooping ? 'Disable loop' : 'Enable loop'}
285
- >
286
- <Repeat className="h-4 w-4" />
287
- </Button>
288
- )}
289
- </div>
290
- )}
291
- </div>
292
- );
293
- }
294
-
295
- export default ProgressiveAudioPlayer;