@djangocfg/ui-nextjs 2.1.78 → 2.1.79
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/README.md +24 -4
- package/package.json +4 -4
- package/src/tools/AudioPlayer/README.md +16 -1
- package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +9 -1
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +12 -3
- package/src/tools/AudioPlayer/hooks/index.ts +2 -0
- package/src/tools/AudioPlayer/hooks/useAudioSource.ts +139 -0
- package/src/tools/AudioPlayer/types/audio.ts +14 -0
package/README.md
CHANGED
|
@@ -57,11 +57,11 @@ All components from `@djangocfg/ui-core` are re-exported.
|
|
|
57
57
|
import { Hero } from '@djangocfg/ui-nextjs/blocks';
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
### Tools (
|
|
61
|
-
`JsonTree` `PrettyCode` `Mermaid` `LottiePlayer`
|
|
60
|
+
### Tools (7)
|
|
61
|
+
`JsonTree` `PrettyCode` `Mermaid` `LottiePlayer` `AudioPlayer` `VideoPlayer` `ImageViewer`
|
|
62
62
|
|
|
63
63
|
```tsx
|
|
64
|
-
import { PrettyCode } from '@djangocfg/ui-nextjs/tools';
|
|
64
|
+
import { PrettyCode, AudioPlayer, VideoPlayer } from '@djangocfg/ui-nextjs/tools';
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
## Hooks
|
|
@@ -108,6 +108,25 @@ function Example() {
|
|
|
108
108
|
import '@djangocfg/ui-nextjs/styles/globals';
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
## Logger
|
|
112
|
+
|
|
113
|
+
Universal logger with consola + zustand for Console panel integration.
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { createLogger, createMediaLogger } from '@djangocfg/ui-nextjs/lib';
|
|
117
|
+
|
|
118
|
+
// Basic logger
|
|
119
|
+
const log = createLogger('MyComponent');
|
|
120
|
+
log.info('User logged in', { userId: 123 });
|
|
121
|
+
log.error('Failed to load', { error });
|
|
122
|
+
|
|
123
|
+
// Media logger (with seek/buffer helpers)
|
|
124
|
+
const mediaLog = createMediaLogger('AudioPlayer');
|
|
125
|
+
mediaLog.load(src, 'stream');
|
|
126
|
+
mediaLog.seek(from, to, duration);
|
|
127
|
+
mediaLog.buffer(buffered, duration);
|
|
128
|
+
```
|
|
129
|
+
|
|
111
130
|
## Exports
|
|
112
131
|
|
|
113
132
|
| Path | Content |
|
|
@@ -116,7 +135,8 @@ import '@djangocfg/ui-nextjs/styles/globals';
|
|
|
116
135
|
| `@djangocfg/ui-nextjs/components` | Components only |
|
|
117
136
|
| `@djangocfg/ui-nextjs/hooks` | Hooks only |
|
|
118
137
|
| `@djangocfg/ui-nextjs/blocks` | Landing page blocks |
|
|
119
|
-
| `@djangocfg/ui-nextjs/tools` | JsonTree, Mermaid,
|
|
138
|
+
| `@djangocfg/ui-nextjs/tools` | JsonTree, Mermaid, Media players |
|
|
139
|
+
| `@djangocfg/ui-nextjs/lib` | Logger, utilities |
|
|
120
140
|
| `@djangocfg/ui-nextjs/theme` | ThemeProvider, ThemeToggle |
|
|
121
141
|
| `@djangocfg/ui-nextjs/styles` | CSS |
|
|
122
142
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.79",
|
|
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.79",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.79",
|
|
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.79",
|
|
114
114
|
"@types/node": "^24.7.2",
|
|
115
115
|
"eslint": "^9.37.0",
|
|
116
116
|
"tailwindcss-animate": "1.0.7",
|
|
@@ -46,6 +46,7 @@ import { SimpleAudioPlayer } from '@djangocfg/ui-nextjs';
|
|
|
46
46
|
| Prop | Type | Default | Description |
|
|
47
47
|
|------|------|---------|-------------|
|
|
48
48
|
| `src` | `string` | required | Audio URL |
|
|
49
|
+
| `prefetch` | `boolean` | `true` | Pre-fetch audio as blob (required for streaming URLs to enable seek) |
|
|
49
50
|
| `title` | `string` | - | Track title |
|
|
50
51
|
| `artist` | `string` | - | Artist name |
|
|
51
52
|
| `coverArt` | `string \| ReactNode` | - | Cover image URL or custom element |
|
|
@@ -54,6 +55,7 @@ import { SimpleAudioPlayer } from '@djangocfg/ui-nextjs';
|
|
|
54
55
|
| `showEqualizer` | `boolean` | `false` | Show equalizer bars |
|
|
55
56
|
| `showTimer` | `boolean` | `true` | Show time display |
|
|
56
57
|
| `showVolume` | `boolean` | `true` | Show volume control |
|
|
58
|
+
| `showLoop` | `boolean` | `true` | Show loop/repeat button |
|
|
57
59
|
| `reactiveCover` | `boolean` | `true` | Enable reactive effects |
|
|
58
60
|
| `variant` | `VisualizationVariant` | - | Effect variant |
|
|
59
61
|
| `intensity` | `EffectIntensity` | - | Effect intensity |
|
|
@@ -61,6 +63,8 @@ import { SimpleAudioPlayer } from '@djangocfg/ui-nextjs';
|
|
|
61
63
|
| `autoPlay` | `boolean` | `false` | Auto-play on load |
|
|
62
64
|
| `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
|
|
63
65
|
|
|
66
|
+
> **Note:** The `prefetch` option is enabled by default. This fetches the entire audio file as a blob before loading into WaveSurfer, which is required for seeking to work correctly with streaming URLs. For very large files (> 50MB), consider using `prefetch={false}` and the Progressive player mode instead.
|
|
67
|
+
|
|
64
68
|
---
|
|
65
69
|
|
|
66
70
|
## Advanced Usage
|
|
@@ -99,7 +103,10 @@ Context provider for audio state. Wraps all audio components.
|
|
|
99
103
|
|
|
100
104
|
```tsx
|
|
101
105
|
<AudioProvider
|
|
102
|
-
source={{
|
|
106
|
+
source={{
|
|
107
|
+
uri: 'https://example.com/audio.mp3',
|
|
108
|
+
prefetch: true // Fetch as blob for seek support (default: false)
|
|
109
|
+
}}
|
|
103
110
|
containerRef={containerRef}
|
|
104
111
|
autoPlay={false}
|
|
105
112
|
waveformOptions={{
|
|
@@ -115,6 +122,13 @@ Context provider for audio state. Wraps all audio components.
|
|
|
115
122
|
</AudioProvider>
|
|
116
123
|
```
|
|
117
124
|
|
|
125
|
+
#### AudioSource Options
|
|
126
|
+
|
|
127
|
+
| Prop | Type | Default | Description |
|
|
128
|
+
|------|------|---------|-------------|
|
|
129
|
+
| `uri` | `string` | required | Audio URL |
|
|
130
|
+
| `prefetch` | `boolean` | `false` | Pre-fetch as blob (enables seek for streaming URLs) |
|
|
131
|
+
|
|
118
132
|
### AudioPlayer
|
|
119
133
|
|
|
120
134
|
Main player component with waveform and controls.
|
|
@@ -294,6 +308,7 @@ AudioPlayer/
|
|
|
294
308
|
│ └── effects.ts # Visualization effect types
|
|
295
309
|
├── hooks/
|
|
296
310
|
│ ├── index.ts
|
|
311
|
+
│ ├── useAudioSource.ts # Audio source loading with prefetch
|
|
297
312
|
│ ├── useAudioHotkeys.ts # Keyboard shortcuts
|
|
298
313
|
│ ├── useVisualization.tsx # Visualization settings
|
|
299
314
|
│ ├── useAudioAnalysis.ts # Web Audio frequency analysis
|
|
@@ -53,6 +53,13 @@ export interface SimpleAudioPlayerProps {
|
|
|
53
53
|
/** Audio source URL */
|
|
54
54
|
src: string;
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Pre-fetch audio as blob before loading into WaveSurfer.
|
|
58
|
+
* Required for streaming URLs because WaveSurfer needs complete file for seek to work.
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
prefetch?: boolean;
|
|
62
|
+
|
|
56
63
|
/** Track title */
|
|
57
64
|
title?: string;
|
|
58
65
|
|
|
@@ -132,6 +139,7 @@ export function SimpleAudioPlayer(props: SimpleAudioPlayerProps) {
|
|
|
132
139
|
|
|
133
140
|
function SimpleAudioPlayerContent({
|
|
134
141
|
src,
|
|
142
|
+
prefetch = true,
|
|
135
143
|
title,
|
|
136
144
|
artist,
|
|
137
145
|
coverArt,
|
|
@@ -190,7 +198,7 @@ function SimpleAudioPlayerContent({
|
|
|
190
198
|
|
|
191
199
|
return (
|
|
192
200
|
<AudioProvider
|
|
193
|
-
source={{ uri: src }}
|
|
201
|
+
source={{ uri: src, prefetch }}
|
|
194
202
|
containerRef={containerRef}
|
|
195
203
|
autoPlay={autoPlay}
|
|
196
204
|
waveformOptions={waveformOptions}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from 'react';
|
|
19
19
|
import { useWavesurfer } from '@wavesurfer/react';
|
|
20
20
|
import type { AudioContextState, AudioSource, WaveformOptions } from '../types';
|
|
21
|
-
import { useSharedWebAudio, useAudioAnalysis } from '../hooks';
|
|
21
|
+
import { useSharedWebAudio, useAudioAnalysis, useAudioSource } from '../hooks';
|
|
22
22
|
import { useAudioCache } from '../../../stores/mediaCache';
|
|
23
23
|
import { audioDebug } from '../utils/debug';
|
|
24
24
|
|
|
@@ -61,11 +61,14 @@ export function AudioProvider({
|
|
|
61
61
|
const { saveAudioPosition, getAudioPosition } = useAudioCache();
|
|
62
62
|
const lastSavedTimeRef = useRef<number>(0);
|
|
63
63
|
|
|
64
|
+
// Handle prefetch if enabled (for streaming URLs)
|
|
65
|
+
const { url: resolvedUrl, isLoading: isPrefetching, progress: prefetchProgress } = useAudioSource(source);
|
|
66
|
+
|
|
64
67
|
// Memoize WaveSurfer options with theme-aware colors
|
|
65
68
|
const options = useMemo(
|
|
66
69
|
() => ({
|
|
67
70
|
container: containerRef,
|
|
68
|
-
url:
|
|
71
|
+
url: resolvedUrl || undefined, // Use prefetched blob URL or original
|
|
69
72
|
// Theme-aware colors using HSL
|
|
70
73
|
waveColor: waveformOptions.waveColor || 'hsl(217 91% 60% / 0.3)',
|
|
71
74
|
progressColor: waveformOptions.progressColor || 'hsl(217 91% 60%)',
|
|
@@ -80,7 +83,7 @@ export function AudioProvider({
|
|
|
80
83
|
hideScrollbar: true,
|
|
81
84
|
autoplay: autoPlay,
|
|
82
85
|
}),
|
|
83
|
-
[
|
|
86
|
+
[resolvedUrl, autoPlay, waveformOptions, containerRef]
|
|
84
87
|
);
|
|
85
88
|
|
|
86
89
|
// Use official wavesurfer-react hook
|
|
@@ -309,6 +312,10 @@ export function AudioProvider({
|
|
|
309
312
|
isMuted,
|
|
310
313
|
isLooping,
|
|
311
314
|
|
|
315
|
+
// Prefetch state
|
|
316
|
+
isPrefetching,
|
|
317
|
+
prefetchProgress,
|
|
318
|
+
|
|
312
319
|
// Audio analysis
|
|
313
320
|
audioLevels,
|
|
314
321
|
|
|
@@ -336,6 +343,8 @@ export function AudioProvider({
|
|
|
336
343
|
volume,
|
|
337
344
|
isMuted,
|
|
338
345
|
isLooping,
|
|
346
|
+
isPrefetching,
|
|
347
|
+
prefetchProgress,
|
|
339
348
|
audioLevels,
|
|
340
349
|
play,
|
|
341
350
|
pause,
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// Internal hooks (used by provider)
|
|
6
6
|
export { useSharedWebAudio } from './useSharedWebAudio';
|
|
7
7
|
export { useAudioAnalysis } from './useAudioAnalysis';
|
|
8
|
+
export { useAudioSource } from './useAudioSource';
|
|
9
|
+
export type { UseAudioSourceResult } from './useAudioSource';
|
|
8
10
|
|
|
9
11
|
// Public hooks
|
|
10
12
|
export { useAudioHotkeys, AUDIO_SHORTCUTS } from './useAudioHotkeys';
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
setUrl(source.uri);
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Prefetch enabled - fetch as blob
|
|
51
|
+
const abortController = new AbortController();
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
|
|
54
|
+
const fetchAsBlob = async () => {
|
|
55
|
+
try {
|
|
56
|
+
audioDebug.info('Prefetching audio as blob', { uri: source.uri });
|
|
57
|
+
|
|
58
|
+
const response = await fetch(source.uri, {
|
|
59
|
+
signal: abortController.signal,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Get content length for progress tracking
|
|
67
|
+
const contentLength = response.headers.get('Content-Length');
|
|
68
|
+
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
|
|
69
|
+
|
|
70
|
+
if (!response.body) {
|
|
71
|
+
// Fallback for browsers without ReadableStream
|
|
72
|
+
const blob = await response.blob();
|
|
73
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
74
|
+
blobUrlRef.current = blobUrl;
|
|
75
|
+
setUrl(blobUrl);
|
|
76
|
+
setProgress(100);
|
|
77
|
+
audioDebug.success('Audio prefetched (no stream)', { size: blob.size });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Stream the response for progress tracking
|
|
82
|
+
const reader = response.body.getReader();
|
|
83
|
+
const chunks: ArrayBuffer[] = [];
|
|
84
|
+
let receivedBytes = 0;
|
|
85
|
+
|
|
86
|
+
while (true) {
|
|
87
|
+
const { done, value } = await reader.read();
|
|
88
|
+
|
|
89
|
+
if (done) break;
|
|
90
|
+
|
|
91
|
+
// Convert Uint8Array to ArrayBuffer for Blob compatibility
|
|
92
|
+
chunks.push(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
|
|
93
|
+
receivedBytes += value.length;
|
|
94
|
+
|
|
95
|
+
if (totalBytes > 0) {
|
|
96
|
+
setProgress(Math.round((receivedBytes / totalBytes) * 100));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Combine chunks into blob
|
|
101
|
+
const blob = new Blob(chunks, { type: 'audio/mpeg' });
|
|
102
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
103
|
+
blobUrlRef.current = blobUrl;
|
|
104
|
+
setUrl(blobUrl);
|
|
105
|
+
setProgress(100);
|
|
106
|
+
|
|
107
|
+
audioDebug.success('Audio prefetched', {
|
|
108
|
+
size: blob.size,
|
|
109
|
+
sizeFormatted: `${(blob.size / 1024 / 1024).toFixed(2)} MB`,
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
113
|
+
return; // Ignore abort errors
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const errorMessage = err instanceof Error ? err.message : 'Failed to prefetch audio';
|
|
117
|
+
audioDebug.error('Failed to prefetch audio', { error: errorMessage, uri: source.uri });
|
|
118
|
+
setError(errorMessage);
|
|
119
|
+
|
|
120
|
+
// Fallback to direct URL (may have seek issues)
|
|
121
|
+
setUrl(source.uri);
|
|
122
|
+
} finally {
|
|
123
|
+
setIsLoading(false);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
fetchAsBlob();
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
abortController.abort();
|
|
131
|
+
if (blobUrlRef.current) {
|
|
132
|
+
URL.revokeObjectURL(blobUrlRef.current);
|
|
133
|
+
blobUrlRef.current = null;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}, [source.uri, source.prefetch]);
|
|
137
|
+
|
|
138
|
+
return { url, isLoading, error, progress };
|
|
139
|
+
}
|
|
@@ -10,7 +10,15 @@ import type { AudioLevels } from './effects';
|
|
|
10
10
|
// =============================================================================
|
|
11
11
|
|
|
12
12
|
export interface AudioSource {
|
|
13
|
+
/** Audio URL (streaming or direct) */
|
|
13
14
|
uri: string;
|
|
15
|
+
/**
|
|
16
|
+
* Pre-fetch the URL as blob before passing to WaveSurfer.
|
|
17
|
+
* Required for streaming URLs because WaveSurfer needs complete file for seek to work.
|
|
18
|
+
* When true, the URL will be fetched and converted to blob URL.
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
prefetch?: boolean;
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
// =============================================================================
|
|
@@ -89,6 +97,12 @@ export interface AudioContextState {
|
|
|
89
97
|
isMuted: boolean;
|
|
90
98
|
isLooping: boolean;
|
|
91
99
|
|
|
100
|
+
// Prefetch state (for streaming URLs)
|
|
101
|
+
/** Whether audio is being prefetched as blob */
|
|
102
|
+
isPrefetching: boolean;
|
|
103
|
+
/** Prefetch progress (0-100) */
|
|
104
|
+
prefetchProgress: number;
|
|
105
|
+
|
|
92
106
|
// Audio analysis (for reactive effects)
|
|
93
107
|
audioLevels: AudioLevels;
|
|
94
108
|
|