@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,381 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * WaveformCanvas - Canvas-based waveform visualization
5
- *
6
- * Features:
7
- * - Progressive rendering as peaks load
8
- * - Playback progress overlay
9
- * - Buffered ranges indicator
10
- * - Interactive seek (click/drag)
11
- * - Hover preview
12
- * - High DPI support
13
- */
14
-
15
- import {
16
- useRef,
17
- useEffect,
18
- useCallback,
19
- useState,
20
- forwardRef,
21
- useImperativeHandle,
22
- } from 'react';
23
- import { cn } from '@djangocfg/ui-core';
24
- import type { WaveformStyle, LoadedRange } from './types';
25
-
26
- // =============================================================================
27
- // TYPES
28
- // =============================================================================
29
-
30
- interface WaveformCanvasProps {
31
- /** Peaks data (0-1 normalized) */
32
- peaks: number[];
33
- /** Current playback time (seconds) */
34
- currentTime: number;
35
- /** Total duration (seconds) */
36
- duration: number;
37
- /** Buffered ranges from audio element */
38
- buffered?: TimeRanges | null;
39
- /** Loaded ranges (for progressive loading indicator) */
40
- loadedRanges?: LoadedRange[];
41
- /** Loading progress (0-100) */
42
- loadingPercent?: number;
43
- /** Seek callback */
44
- onSeek?: (time: number) => void;
45
- /** Hover callback */
46
- onHover?: (time: number | null) => void;
47
- /** Is interactive */
48
- interactive?: boolean;
49
- /** Style options */
50
- style?: WaveformStyle;
51
- /** Additional class */
52
- className?: string;
53
- }
54
-
55
- export interface WaveformCanvasRef {
56
- redraw: () => void;
57
- }
58
-
59
- // =============================================================================
60
- // DEFAULT STYLES
61
- // =============================================================================
62
-
63
- const DEFAULT_STYLE: Required<WaveformStyle> = {
64
- waveColor: 'hsl(217 91% 60% / 0.3)',
65
- progressColor: 'hsl(217 91% 60%)',
66
- loadingColor: 'hsl(217 91% 60% / 0.1)',
67
- cursorColor: 'hsl(217 91% 60%)',
68
- barWidth: 3,
69
- barGap: 2,
70
- barRadius: 2,
71
- height: 64,
72
- };
73
-
74
- // =============================================================================
75
- // COMPONENT
76
- // =============================================================================
77
-
78
- export const WaveformCanvas = forwardRef<WaveformCanvasRef, WaveformCanvasProps>(
79
- function WaveformCanvas(props, ref) {
80
- const {
81
- peaks,
82
- currentTime,
83
- duration,
84
- buffered,
85
- loadedRanges = [],
86
- loadingPercent = 100,
87
- onSeek,
88
- onHover,
89
- interactive = true,
90
- style = {},
91
- className,
92
- } = props;
93
-
94
- const canvasRef = useRef<HTMLCanvasElement>(null);
95
- const containerRef = useRef<HTMLDivElement>(null);
96
- const [hoverX, setHoverX] = useState<number | null>(null);
97
- const [isDragging, setIsDragging] = useState(false);
98
-
99
- // Merge styles
100
- const {
101
- waveColor,
102
- progressColor,
103
- loadingColor,
104
- cursorColor,
105
- barWidth,
106
- barGap,
107
- barRadius,
108
- height,
109
- } = { ...DEFAULT_STYLE, ...style };
110
-
111
- // ==========================================================================
112
- // DRAWING
113
- // ==========================================================================
114
-
115
- const draw = useCallback(() => {
116
- const canvas = canvasRef.current;
117
- const container = containerRef.current;
118
- if (!canvas || !container) return;
119
-
120
- const ctx = canvas.getContext('2d');
121
- if (!ctx) return;
122
-
123
- // Get dimensions
124
- const rect = container.getBoundingClientRect();
125
- const width = rect.width;
126
- const dpr = window.devicePixelRatio || 1;
127
-
128
- // Set canvas size with DPI scaling
129
- canvas.width = width * dpr;
130
- canvas.height = height * dpr;
131
- canvas.style.width = `${width}px`;
132
- canvas.style.height = `${height}px`;
133
- ctx.scale(dpr, dpr);
134
-
135
- // Clear
136
- ctx.clearRect(0, 0, width, height);
137
-
138
- // Calculate bar positions
139
- const totalBarSpace = barWidth + barGap;
140
- const barCount = Math.floor(width / totalBarSpace);
141
-
142
- if (barCount === 0) return;
143
-
144
- // Progress position
145
- const progress = duration > 0 ? currentTime / duration : 0;
146
- const progressX = progress * width;
147
-
148
- // Hover position
149
- const hoverProgress = hoverX !== null ? hoverX / width : null;
150
-
151
- // Draw loading placeholder for unloaded area
152
- if (loadingPercent < 100) {
153
- const loadedX = (loadingPercent / 100) * width;
154
- ctx.fillStyle = loadingColor;
155
- ctx.fillRect(loadedX, 0, width - loadedX, height);
156
- }
157
-
158
- // Resample peaks to match bar count
159
- const resampledPeaks = resamplePeaks(peaks, barCount);
160
-
161
- // Draw bars
162
- for (let i = 0; i < barCount; i++) {
163
- const x = i * totalBarSpace;
164
- const peakValue = resampledPeaks[i] || 0;
165
-
166
- // Bar height (minimum 2px for visibility)
167
- const barHeight = Math.max(2, peakValue * (height - 4));
168
- const y = (height - barHeight) / 2;
169
-
170
- // Determine color based on position
171
- const barProgress = (x + barWidth / 2) / width;
172
- let color = waveColor;
173
-
174
- if (barProgress <= progress) {
175
- color = progressColor;
176
- }
177
-
178
- // Draw bar
179
- ctx.fillStyle = color;
180
- if (barRadius > 0) {
181
- roundRect(ctx, x, y, barWidth, barHeight, barRadius);
182
- } else {
183
- ctx.fillRect(x, y, barWidth, barHeight);
184
- }
185
- }
186
-
187
- // Draw cursor
188
- if (duration > 0) {
189
- ctx.fillStyle = cursorColor;
190
- ctx.fillRect(progressX - 1, 0, 2, height);
191
- }
192
-
193
- // Draw hover indicator
194
- if (hoverProgress !== null && interactive) {
195
- ctx.fillStyle = `${cursorColor}80`; // 50% opacity
196
- const hoverPx = hoverProgress * width;
197
- ctx.fillRect(hoverPx - 1, 0, 2, height);
198
- }
199
-
200
- // Draw buffered indicator (thin line at bottom)
201
- if (buffered && buffered.length > 0 && duration > 0) {
202
- ctx.fillStyle = `${progressColor}40`;
203
- for (let i = 0; i < buffered.length; i++) {
204
- const start = (buffered.start(i) / duration) * width;
205
- const end = (buffered.end(i) / duration) * width;
206
- ctx.fillRect(start, height - 2, end - start, 2);
207
- }
208
- }
209
- }, [
210
- peaks,
211
- currentTime,
212
- duration,
213
- buffered,
214
- loadingPercent,
215
- hoverX,
216
- waveColor,
217
- progressColor,
218
- loadingColor,
219
- cursorColor,
220
- barWidth,
221
- barGap,
222
- barRadius,
223
- height,
224
- interactive,
225
- ]);
226
-
227
- // ==========================================================================
228
- // INTERACTION HANDLERS
229
- // ==========================================================================
230
-
231
- const getTimeFromX = useCallback((clientX: number): number => {
232
- const container = containerRef.current;
233
- if (!container || duration <= 0) return 0;
234
-
235
- const rect = container.getBoundingClientRect();
236
- const x = clientX - rect.left;
237
- const progress = Math.max(0, Math.min(1, x / rect.width));
238
- return progress * duration;
239
- }, [duration]);
240
-
241
- const handleMouseMove = useCallback((e: React.MouseEvent) => {
242
- if (!interactive) return;
243
-
244
- const container = containerRef.current;
245
- if (!container) return;
246
-
247
- const rect = container.getBoundingClientRect();
248
- const x = e.clientX - rect.left;
249
- setHoverX(x);
250
-
251
- const time = getTimeFromX(e.clientX);
252
- onHover?.(time);
253
-
254
- if (isDragging) {
255
- onSeek?.(time);
256
- }
257
- }, [interactive, isDragging, getTimeFromX, onHover, onSeek]);
258
-
259
- const handleMouseLeave = useCallback(() => {
260
- setHoverX(null);
261
- onHover?.(null);
262
- setIsDragging(false);
263
- }, [onHover]);
264
-
265
- const handleMouseDown = useCallback((e: React.MouseEvent) => {
266
- if (!interactive) return;
267
- e.preventDefault();
268
- setIsDragging(true);
269
- const time = getTimeFromX(e.clientX);
270
- onSeek?.(time);
271
- }, [interactive, getTimeFromX, onSeek]);
272
-
273
- const handleMouseUp = useCallback(() => {
274
- setIsDragging(false);
275
- }, []);
276
-
277
- // Global mouseup for drag release
278
- useEffect(() => {
279
- if (!isDragging) return;
280
-
281
- const handleGlobalMouseUp = () => setIsDragging(false);
282
- window.addEventListener('mouseup', handleGlobalMouseUp);
283
- return () => window.removeEventListener('mouseup', handleGlobalMouseUp);
284
- }, [isDragging]);
285
-
286
- // ==========================================================================
287
- // EFFECTS
288
- // ==========================================================================
289
-
290
- // Redraw on any change
291
- useEffect(() => {
292
- draw();
293
- }, [draw]);
294
-
295
- // Redraw on resize
296
- useEffect(() => {
297
- const handleResize = () => draw();
298
- window.addEventListener('resize', handleResize);
299
- return () => window.removeEventListener('resize', handleResize);
300
- }, [draw]);
301
-
302
- // Expose redraw method
303
- useImperativeHandle(ref, () => ({
304
- redraw: draw,
305
- }), [draw]);
306
-
307
- return (
308
- <div
309
- ref={containerRef}
310
- className={cn(
311
- 'relative w-full cursor-pointer select-none',
312
- !interactive && 'cursor-default',
313
- className
314
- )}
315
- style={{ height }}
316
- onMouseMove={handleMouseMove}
317
- onMouseLeave={handleMouseLeave}
318
- onMouseDown={handleMouseDown}
319
- onMouseUp={handleMouseUp}
320
- >
321
- <canvas
322
- ref={canvasRef}
323
- className="absolute inset-0"
324
- />
325
- </div>
326
- );
327
- }
328
- );
329
-
330
- // =============================================================================
331
- // HELPERS
332
- // =============================================================================
333
-
334
- /**
335
- * Draw rounded rectangle
336
- */
337
- function roundRect(
338
- ctx: CanvasRenderingContext2D,
339
- x: number,
340
- y: number,
341
- width: number,
342
- height: number,
343
- radius: number
344
- ) {
345
- ctx.beginPath();
346
- ctx.moveTo(x + radius, y);
347
- ctx.lineTo(x + width - radius, y);
348
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
349
- ctx.lineTo(x + width, y + height - radius);
350
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
351
- ctx.lineTo(x + radius, y + height);
352
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
353
- ctx.lineTo(x, y + radius);
354
- ctx.quadraticCurveTo(x, y, x + radius, y);
355
- ctx.closePath();
356
- ctx.fill();
357
- }
358
-
359
- /**
360
- * Resample peaks to target length
361
- */
362
- function resamplePeaks(peaks: number[], targetLength: number): number[] {
363
- if (peaks.length === 0) return new Array(targetLength).fill(0);
364
- if (peaks.length === targetLength) return peaks;
365
-
366
- const result = new Array<number>(targetLength);
367
- const ratio = peaks.length / targetLength;
368
-
369
- for (let i = 0; i < targetLength; i++) {
370
- const start = Math.floor(i * ratio);
371
- const end = Math.min(Math.ceil((i + 1) * ratio), peaks.length);
372
-
373
- let max = 0;
374
- for (let j = start; j < end; j++) {
375
- if (peaks[j] > max) max = peaks[j];
376
- }
377
- result[i] = max;
378
- }
379
-
380
- return result;
381
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * Progressive Audio Player
3
- *
4
- * Audio player with progressive waveform loading.
5
- * Uses native HTML5 audio (supports Range requests) and custom canvas visualization.
6
- */
7
-
8
- // Components
9
- export { ProgressiveAudioPlayer } from './ProgressiveAudioPlayer';
10
- export { WaveformCanvas } from './WaveformCanvas';
11
-
12
- // Hooks
13
- export { useAudioElement } from './useAudioElement';
14
- export { useProgressiveWaveform } from './useProgressiveWaveform';
15
-
16
- // Utilities
17
- export {
18
- extractPeaks,
19
- mergePeaks,
20
- resamplePeaks,
21
- smoothPeaks,
22
- calculatePeaksCount,
23
- timeToIndex,
24
- indexToTime,
25
- } from './peaks';
26
-
27
- // Types
28
- export type {
29
- WaveformData,
30
- LoadedRange,
31
- WaveformLoadingState,
32
- AudioState,
33
- AudioControls,
34
- WaveformStyle,
35
- WaveformInteraction,
36
- ProgressiveAudioPlayerProps,
37
- ChunkInfo,
38
- DecodeResult,
39
- DecoderOptions,
40
- } from './types';
@@ -1,234 +0,0 @@
1
- /**
2
- * Audio Peaks Extraction Utilities
3
- *
4
- * Extracts waveform peaks from AudioBuffer for visualization.
5
- */
6
-
7
- // =============================================================================
8
- // TYPES
9
- // =============================================================================
10
-
11
- interface ExtractPeaksOptions {
12
- /** Number of peaks to extract */
13
- length: number;
14
- /** Channel to use (0 = left, 1 = right, -1 = mix) */
15
- channel?: number;
16
- /** Normalize peaks to 0-1 range */
17
- normalize?: boolean;
18
- }
19
-
20
- // =============================================================================
21
- // PEAK EXTRACTION
22
- // =============================================================================
23
-
24
- /**
25
- * Extract peaks from an AudioBuffer
26
- *
27
- * @param audioBuffer - Decoded audio buffer
28
- * @param options - Extraction options
29
- * @returns Array of normalized peak values (0-1)
30
- */
31
- export function extractPeaks(
32
- audioBuffer: AudioBuffer,
33
- options: ExtractPeaksOptions
34
- ): number[] {
35
- const { length, channel = -1, normalize = true } = options;
36
-
37
- // Get channel data
38
- let data: Float32Array;
39
- if (channel === -1) {
40
- // Mix all channels
41
- data = mixChannels(audioBuffer);
42
- } else {
43
- data = audioBuffer.getChannelData(Math.min(channel, audioBuffer.numberOfChannels - 1));
44
- }
45
-
46
- const peaks = new Array<number>(length);
47
- const samplesPerPeak = Math.floor(data.length / length);
48
-
49
- let maxPeak = 0;
50
-
51
- for (let i = 0; i < length; i++) {
52
- const start = i * samplesPerPeak;
53
- const end = Math.min(start + samplesPerPeak, data.length);
54
-
55
- let max = 0;
56
- for (let j = start; j < end; j++) {
57
- const abs = Math.abs(data[j]);
58
- if (abs > max) max = abs;
59
- }
60
-
61
- peaks[i] = max;
62
- if (max > maxPeak) maxPeak = max;
63
- }
64
-
65
- // Normalize to 0-1
66
- if (normalize && maxPeak > 0) {
67
- for (let i = 0; i < length; i++) {
68
- peaks[i] = peaks[i] / maxPeak;
69
- }
70
- }
71
-
72
- return peaks;
73
- }
74
-
75
- /**
76
- * Mix all channels into mono
77
- */
78
- function mixChannels(audioBuffer: AudioBuffer): Float32Array {
79
- const numChannels = audioBuffer.numberOfChannels;
80
- const length = audioBuffer.length;
81
- const mixed = new Float32Array(length);
82
-
83
- for (let ch = 0; ch < numChannels; ch++) {
84
- const channelData = audioBuffer.getChannelData(ch);
85
- for (let i = 0; i < length; i++) {
86
- mixed[i] += channelData[i];
87
- }
88
- }
89
-
90
- // Average
91
- const divisor = numChannels;
92
- for (let i = 0; i < length; i++) {
93
- mixed[i] /= divisor;
94
- }
95
-
96
- return mixed;
97
- }
98
-
99
- // =============================================================================
100
- // PEAK MERGING
101
- // =============================================================================
102
-
103
- /**
104
- * Merge two peaks arrays (for progressive loading)
105
- *
106
- * @param existing - Existing peaks array
107
- * @param newPeaks - New peaks to merge
108
- * @param startIndex - Start index for new peaks
109
- * @returns Merged peaks array
110
- */
111
- export function mergePeaks(
112
- existing: number[],
113
- newPeaks: number[],
114
- startIndex: number
115
- ): number[] {
116
- const result = [...existing];
117
-
118
- // Extend if needed
119
- const neededLength = startIndex + newPeaks.length;
120
- while (result.length < neededLength) {
121
- result.push(0);
122
- }
123
-
124
- // Merge
125
- for (let i = 0; i < newPeaks.length; i++) {
126
- result[startIndex + i] = newPeaks[i];
127
- }
128
-
129
- return result;
130
- }
131
-
132
- // =============================================================================
133
- // PEAK RESAMPLING
134
- // =============================================================================
135
-
136
- /**
137
- * Resample peaks to a different length
138
- *
139
- * @param peaks - Source peaks
140
- * @param targetLength - Target number of peaks
141
- * @returns Resampled peaks
142
- */
143
- export function resamplePeaks(peaks: number[], targetLength: number): number[] {
144
- if (peaks.length === 0) return new Array(targetLength).fill(0);
145
- if (peaks.length === targetLength) return [...peaks];
146
-
147
- const result = new Array<number>(targetLength);
148
- const ratio = peaks.length / targetLength;
149
-
150
- for (let i = 0; i < targetLength; i++) {
151
- const start = Math.floor(i * ratio);
152
- const end = Math.min(Math.ceil((i + 1) * ratio), peaks.length);
153
-
154
- let max = 0;
155
- for (let j = start; j < end; j++) {
156
- if (peaks[j] > max) max = peaks[j];
157
- }
158
- result[i] = max;
159
- }
160
-
161
- return result;
162
- }
163
-
164
- // =============================================================================
165
- // PEAK SMOOTHING
166
- // =============================================================================
167
-
168
- /**
169
- * Smooth peaks using moving average
170
- *
171
- * @param peaks - Source peaks
172
- * @param windowSize - Smoothing window size
173
- * @returns Smoothed peaks
174
- */
175
- export function smoothPeaks(peaks: number[], windowSize: number = 3): number[] {
176
- if (windowSize < 2 || peaks.length < windowSize) return [...peaks];
177
-
178
- const result = new Array<number>(peaks.length);
179
- const halfWindow = Math.floor(windowSize / 2);
180
-
181
- for (let i = 0; i < peaks.length; i++) {
182
- const start = Math.max(0, i - halfWindow);
183
- const end = Math.min(peaks.length, i + halfWindow + 1);
184
-
185
- let sum = 0;
186
- for (let j = start; j < end; j++) {
187
- sum += peaks[j];
188
- }
189
- result[i] = sum / (end - start);
190
- }
191
-
192
- return result;
193
- }
194
-
195
- // =============================================================================
196
- // PEAK CALCULATIONS
197
- // =============================================================================
198
-
199
- /**
200
- * Calculate peaks per second based on width and duration
201
- */
202
- export function calculatePeaksCount(
203
- width: number,
204
- barWidth: number,
205
- barGap: number
206
- ): number {
207
- const totalBarSpace = barWidth + barGap;
208
- return Math.floor(width / totalBarSpace);
209
- }
210
-
211
- /**
212
- * Calculate which peak index corresponds to a time
213
- */
214
- export function timeToIndex(
215
- time: number,
216
- duration: number,
217
- peaksCount: number
218
- ): number {
219
- if (duration <= 0) return 0;
220
- const progress = time / duration;
221
- return Math.floor(progress * peaksCount);
222
- }
223
-
224
- /**
225
- * Calculate time from peak index
226
- */
227
- export function indexToTime(
228
- index: number,
229
- peaksCount: number,
230
- duration: number
231
- ): number {
232
- if (peaksCount <= 0) return 0;
233
- return (index / peaksCount) * duration;
234
- }