@djangocfg/ui-nextjs 2.1.64 → 2.1.66
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 +9 -6
- package/src/blocks/SplitHero/SplitHeroMedia.tsx +2 -1
- package/src/tools/AudioPlayer/AudioEqualizer.tsx +235 -0
- package/src/tools/AudioPlayer/AudioPlayer.tsx +223 -0
- package/src/tools/AudioPlayer/AudioReactiveCover.tsx +389 -0
- package/src/tools/AudioPlayer/AudioShortcutsPopover.tsx +95 -0
- package/src/tools/AudioPlayer/README.md +301 -0
- package/src/tools/AudioPlayer/SimpleAudioPlayer.tsx +275 -0
- package/src/tools/AudioPlayer/VisualizationToggle.tsx +68 -0
- package/src/tools/AudioPlayer/context.tsx +426 -0
- package/src/tools/AudioPlayer/effects/index.ts +412 -0
- package/src/tools/AudioPlayer/index.ts +84 -0
- package/src/tools/AudioPlayer/types.ts +162 -0
- package/src/tools/AudioPlayer/useAudioHotkeys.ts +142 -0
- package/src/tools/AudioPlayer/useAudioVisualization.tsx +195 -0
- package/src/tools/ImageViewer/ImageViewer.tsx +416 -0
- package/src/tools/ImageViewer/README.md +161 -0
- package/src/tools/ImageViewer/index.ts +16 -0
- package/src/tools/VideoPlayer/README.md +196 -187
- package/src/tools/VideoPlayer/VideoErrorFallback.tsx +174 -0
- package/src/tools/VideoPlayer/VideoPlayer.tsx +189 -218
- package/src/tools/VideoPlayer/VideoPlayerContext.tsx +125 -0
- package/src/tools/VideoPlayer/index.ts +59 -7
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +206 -0
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +311 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +254 -0
- package/src/tools/VideoPlayer/providers/index.ts +8 -0
- package/src/tools/VideoPlayer/types.ts +320 -71
- package/src/tools/index.ts +82 -4
- package/src/tools/VideoPlayer/NativePlayer.tsx +0 -141
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# AudioPlayer
|
|
2
|
+
|
|
3
|
+
Audio player with waveform visualization, built on WaveSurfer.js.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Waveform visualization with interactive seeking
|
|
8
|
+
- Audio-reactive cover effects (glow, orbs, spotlight, mesh)
|
|
9
|
+
- Real-time frequency equalizer
|
|
10
|
+
- Keyboard shortcuts
|
|
11
|
+
- Volume control with mute
|
|
12
|
+
- Skip forward/backward
|
|
13
|
+
- Playback speed control
|
|
14
|
+
|
|
15
|
+
## Quick Start (Recommended)
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { SimpleAudioPlayer } from '@djangocfg/ui-nextjs';
|
|
19
|
+
|
|
20
|
+
// Minimal
|
|
21
|
+
<SimpleAudioPlayer src="https://example.com/audio.mp3" />
|
|
22
|
+
|
|
23
|
+
// With metadata
|
|
24
|
+
<SimpleAudioPlayer
|
|
25
|
+
src={audioUrl}
|
|
26
|
+
title="Track Title"
|
|
27
|
+
artist="Artist Name"
|
|
28
|
+
coverArt="/path/to/cover.jpg"
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
// Full customization
|
|
32
|
+
<SimpleAudioPlayer
|
|
33
|
+
src={audioUrl}
|
|
34
|
+
title="Track Title"
|
|
35
|
+
coverArt={coverUrl}
|
|
36
|
+
showWaveform
|
|
37
|
+
showEqualizer={false}
|
|
38
|
+
reactiveCover
|
|
39
|
+
variant="spotlight"
|
|
40
|
+
layout="horizontal"
|
|
41
|
+
/>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### SimpleAudioPlayer Props
|
|
45
|
+
|
|
46
|
+
| Prop | Type | Default | Description |
|
|
47
|
+
|------|------|---------|-------------|
|
|
48
|
+
| `src` | `string` | required | Audio URL |
|
|
49
|
+
| `title` | `string` | - | Track title |
|
|
50
|
+
| `artist` | `string` | - | Artist name |
|
|
51
|
+
| `coverArt` | `string \| ReactNode` | - | Cover image URL or custom element |
|
|
52
|
+
| `coverSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Cover art size |
|
|
53
|
+
| `showWaveform` | `boolean` | `true` | Show waveform |
|
|
54
|
+
| `showEqualizer` | `boolean` | `false` | Show equalizer bars |
|
|
55
|
+
| `showTimer` | `boolean` | `true` | Show time display |
|
|
56
|
+
| `showVolume` | `boolean` | `true` | Show volume control |
|
|
57
|
+
| `reactiveCover` | `boolean` | `true` | Enable reactive effects |
|
|
58
|
+
| `variant` | `VisualizationVariant` | - | Effect variant |
|
|
59
|
+
| `intensity` | `EffectIntensity` | - | Effect intensity |
|
|
60
|
+
| `colorScheme` | `EffectColorScheme` | - | Effect colors |
|
|
61
|
+
| `autoPlay` | `boolean` | `false` | Auto-play on load |
|
|
62
|
+
| `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Advanced Usage
|
|
67
|
+
|
|
68
|
+
For full control, use `AudioProvider` + `AudioPlayer` directly:
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { useRef } from 'react';
|
|
72
|
+
import { AudioProvider, AudioPlayer } from '@djangocfg/ui-nextjs';
|
|
73
|
+
|
|
74
|
+
function MyAudioPlayer({ audioUrl }: { audioUrl: string }) {
|
|
75
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<AudioProvider
|
|
79
|
+
source={{ uri: audioUrl }}
|
|
80
|
+
containerRef={containerRef}
|
|
81
|
+
>
|
|
82
|
+
<AudioPlayer
|
|
83
|
+
ref={containerRef}
|
|
84
|
+
showControls
|
|
85
|
+
showWaveform
|
|
86
|
+
showTimer
|
|
87
|
+
showVolume
|
|
88
|
+
/>
|
|
89
|
+
</AudioProvider>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Components
|
|
95
|
+
|
|
96
|
+
### AudioProvider
|
|
97
|
+
|
|
98
|
+
Context provider for audio state. Wraps all audio components.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
<AudioProvider
|
|
102
|
+
source={{ uri: 'https://example.com/audio.mp3' }}
|
|
103
|
+
containerRef={containerRef}
|
|
104
|
+
autoPlay={false}
|
|
105
|
+
waveformOptions={{
|
|
106
|
+
waveColor: 'hsl(217 91% 60% / 0.3)',
|
|
107
|
+
progressColor: 'hsl(217 91% 60%)',
|
|
108
|
+
height: 64,
|
|
109
|
+
barWidth: 3,
|
|
110
|
+
barRadius: 3,
|
|
111
|
+
barGap: 2,
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{children}
|
|
115
|
+
</AudioProvider>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### AudioPlayer
|
|
119
|
+
|
|
120
|
+
Main player component with waveform and controls.
|
|
121
|
+
|
|
122
|
+
| Prop | Type | Default | Description |
|
|
123
|
+
|------|------|---------|-------------|
|
|
124
|
+
| `showControls` | `boolean` | `true` | Show playback controls |
|
|
125
|
+
| `showWaveform` | `boolean` | `true` | Show waveform visualization |
|
|
126
|
+
| `showEqualizer` | `boolean` | `false` | Show equalizer animation |
|
|
127
|
+
| `showTimer` | `boolean` | `true` | Show time display |
|
|
128
|
+
| `showVolume` | `boolean` | `true` | Show volume slider |
|
|
129
|
+
| `className` | `string` | - | Additional CSS class |
|
|
130
|
+
|
|
131
|
+
### AudioEqualizer
|
|
132
|
+
|
|
133
|
+
Real-time frequency visualizer with animated bars.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<AudioEqualizer
|
|
137
|
+
barCount={24}
|
|
138
|
+
height={48}
|
|
139
|
+
gap={2}
|
|
140
|
+
showPeaks
|
|
141
|
+
barColor="hsl(217 91% 60%)"
|
|
142
|
+
peakColor="hsl(217 91% 70%)"
|
|
143
|
+
/>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### AudioReactiveCover
|
|
147
|
+
|
|
148
|
+
Album art wrapper with audio-reactive effects.
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
<AudioReactiveCover
|
|
152
|
+
variant="spotlight" // 'glow' | 'orbs' | 'spotlight' | 'mesh'
|
|
153
|
+
intensity="medium" // 'subtle' | 'medium' | 'strong'
|
|
154
|
+
colorScheme="primary" // 'primary' | 'vibrant' | 'cool' | 'warm'
|
|
155
|
+
onClick={() => nextVariant()}
|
|
156
|
+
>
|
|
157
|
+
<img src={coverArt} alt="Album cover" />
|
|
158
|
+
</AudioReactiveCover>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### VisualizationToggle
|
|
162
|
+
|
|
163
|
+
Button to cycle through visualization variants.
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<VisualizationToggle compact />
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Hooks
|
|
170
|
+
|
|
171
|
+
### useAudio
|
|
172
|
+
|
|
173
|
+
Access full audio state and controls.
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
const {
|
|
177
|
+
isReady,
|
|
178
|
+
isPlaying,
|
|
179
|
+
currentTime,
|
|
180
|
+
duration,
|
|
181
|
+
volume,
|
|
182
|
+
isMuted,
|
|
183
|
+
audioLevels, // { bass, mid, high, overall }
|
|
184
|
+
play,
|
|
185
|
+
pause,
|
|
186
|
+
togglePlay,
|
|
187
|
+
seek,
|
|
188
|
+
seekTo,
|
|
189
|
+
skip,
|
|
190
|
+
setVolume,
|
|
191
|
+
toggleMute,
|
|
192
|
+
restart,
|
|
193
|
+
} = useAudio();
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### useAudioControls
|
|
197
|
+
|
|
198
|
+
Controls only (no re-render on time updates).
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
const { play, pause, togglePlay, skip, restart } = useAudioControls();
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### useAudioState
|
|
205
|
+
|
|
206
|
+
Read-only playback state.
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
const { isPlaying, currentTime, duration, volume } = useAudioState();
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### useAudioElement
|
|
213
|
+
|
|
214
|
+
Access audio element for custom visualizations.
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
const { audioElement, isPlaying, audioLevels } = useAudioElement();
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### useAudioVisualization
|
|
221
|
+
|
|
222
|
+
Manage visualization settings (persisted in localStorage).
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
const {
|
|
226
|
+
settings, // { enabled, variant, intensity, colorScheme }
|
|
227
|
+
toggle,
|
|
228
|
+
setSetting,
|
|
229
|
+
nextVariant,
|
|
230
|
+
nextIntensity,
|
|
231
|
+
nextColorScheme,
|
|
232
|
+
reset,
|
|
233
|
+
} = useAudioVisualization();
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### useAudioHotkeys
|
|
237
|
+
|
|
238
|
+
Enable keyboard shortcuts.
|
|
239
|
+
|
|
240
|
+
```tsx
|
|
241
|
+
useAudioHotkeys({
|
|
242
|
+
enabled: true,
|
|
243
|
+
skipDuration: 10,
|
|
244
|
+
volumeStep: 0.1,
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Keyboard Shortcuts
|
|
249
|
+
|
|
250
|
+
| Key | Action |
|
|
251
|
+
|-----|--------|
|
|
252
|
+
| `Space` | Play/Pause |
|
|
253
|
+
| `←` / `J` | Skip 10s backward |
|
|
254
|
+
| `→` / `L` | Skip 10s forward |
|
|
255
|
+
| `↑` | Volume up |
|
|
256
|
+
| `↓` | Volume down |
|
|
257
|
+
| `M` | Mute/Unmute |
|
|
258
|
+
| `0-9` | Jump to 0-90% |
|
|
259
|
+
|
|
260
|
+
## Waveform Options
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
interface WaveformOptions {
|
|
264
|
+
waveColor?: string; // Unplayed wave color
|
|
265
|
+
progressColor?: string; // Played wave color
|
|
266
|
+
cursorColor?: string; // Playhead cursor color
|
|
267
|
+
cursorWidth?: number; // Cursor width in px
|
|
268
|
+
height?: number; // Waveform height in px
|
|
269
|
+
barWidth?: number; // Bar width in px
|
|
270
|
+
barRadius?: number; // Bar corner radius
|
|
271
|
+
barGap?: number; // Gap between bars
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Effect Variants
|
|
276
|
+
|
|
277
|
+
| Variant | Description |
|
|
278
|
+
|---------|-------------|
|
|
279
|
+
| `spotlight` | Rotating conic gradient with bass pulse |
|
|
280
|
+
| `glow` | Multi-layered radial glows from edges |
|
|
281
|
+
| `orbs` | Floating orbs that react to frequencies |
|
|
282
|
+
| `mesh` | Large gradient blobs with movement |
|
|
283
|
+
| `none` | Effects disabled |
|
|
284
|
+
|
|
285
|
+
## Architecture
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
AudioPlayer/
|
|
289
|
+
├── index.ts # Exports
|
|
290
|
+
├── types.ts # Type definitions
|
|
291
|
+
├── context.tsx # AudioProvider, useAudio hooks
|
|
292
|
+
├── AudioPlayer.tsx # Main player component
|
|
293
|
+
├── AudioEqualizer.tsx # Frequency bars
|
|
294
|
+
├── AudioReactiveCover.tsx # Reactive album art
|
|
295
|
+
├── AudioShortcutsPopover.tsx # Shortcuts help
|
|
296
|
+
├── VisualizationToggle.tsx # Effect toggle button
|
|
297
|
+
├── useAudioHotkeys.ts # Keyboard shortcuts
|
|
298
|
+
├── useAudioVisualization.tsx # Visualization settings
|
|
299
|
+
└── effects/
|
|
300
|
+
└── index.ts # Effect calculations
|
|
301
|
+
```
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SimpleAudioPlayer - Easy-to-use audio player wrapper
|
|
5
|
+
*
|
|
6
|
+
* Combines AudioProvider + AudioPlayer + optional reactive cover
|
|
7
|
+
* in a single component with sensible defaults.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Minimal usage
|
|
11
|
+
* <SimpleAudioPlayer src="https://example.com/audio.mp3" />
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // With cover art
|
|
15
|
+
* <SimpleAudioPlayer
|
|
16
|
+
* src={audioUrl}
|
|
17
|
+
* title="Track Title"
|
|
18
|
+
* artist="Artist Name"
|
|
19
|
+
* coverArt="/path/to/cover.jpg"
|
|
20
|
+
* />
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Full customization
|
|
24
|
+
* <SimpleAudioPlayer
|
|
25
|
+
* src={audioUrl}
|
|
26
|
+
* title="Track Title"
|
|
27
|
+
* coverArt={coverUrl}
|
|
28
|
+
* showWaveform
|
|
29
|
+
* showEqualizer={false}
|
|
30
|
+
* reactiveCover
|
|
31
|
+
* variant="spotlight"
|
|
32
|
+
* autoPlay={false}
|
|
33
|
+
* />
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { useRef, type ReactNode } from 'react';
|
|
37
|
+
import { Music } from 'lucide-react';
|
|
38
|
+
import { cn } from '@djangocfg/ui-core';
|
|
39
|
+
|
|
40
|
+
import { AudioProvider } from './context';
|
|
41
|
+
import { AudioPlayer } from './AudioPlayer';
|
|
42
|
+
import { AudioReactiveCover } from './AudioReactiveCover';
|
|
43
|
+
import { VisualizationProvider, useAudioVisualization } from './useAudioVisualization';
|
|
44
|
+
import type { WaveformOptions, EqualizerOptions } from './types';
|
|
45
|
+
import type { EffectIntensity, EffectColorScheme } from './effects';
|
|
46
|
+
import type { VisualizationVariant } from './useAudioVisualization';
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// TYPES
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
export interface SimpleAudioPlayerProps {
|
|
53
|
+
/** Audio source URL */
|
|
54
|
+
src: string;
|
|
55
|
+
|
|
56
|
+
/** Track title */
|
|
57
|
+
title?: string;
|
|
58
|
+
|
|
59
|
+
/** Artist name */
|
|
60
|
+
artist?: string;
|
|
61
|
+
|
|
62
|
+
/** Cover art URL or ReactNode */
|
|
63
|
+
coverArt?: string | ReactNode;
|
|
64
|
+
|
|
65
|
+
/** Cover art size */
|
|
66
|
+
coverSize?: 'sm' | 'md' | 'lg';
|
|
67
|
+
|
|
68
|
+
/** Show waveform visualization */
|
|
69
|
+
showWaveform?: boolean;
|
|
70
|
+
|
|
71
|
+
/** Show equalizer bars */
|
|
72
|
+
showEqualizer?: boolean;
|
|
73
|
+
|
|
74
|
+
/** Show timer */
|
|
75
|
+
showTimer?: boolean;
|
|
76
|
+
|
|
77
|
+
/** Show volume control */
|
|
78
|
+
showVolume?: boolean;
|
|
79
|
+
|
|
80
|
+
/** Enable audio-reactive cover effects */
|
|
81
|
+
reactiveCover?: boolean;
|
|
82
|
+
|
|
83
|
+
/** Reactive effect variant */
|
|
84
|
+
variant?: VisualizationVariant;
|
|
85
|
+
|
|
86
|
+
/** Reactive effect intensity */
|
|
87
|
+
intensity?: EffectIntensity;
|
|
88
|
+
|
|
89
|
+
/** Reactive effect color scheme */
|
|
90
|
+
colorScheme?: EffectColorScheme;
|
|
91
|
+
|
|
92
|
+
/** Auto-play on load */
|
|
93
|
+
autoPlay?: boolean;
|
|
94
|
+
|
|
95
|
+
/** Waveform customization */
|
|
96
|
+
waveformOptions?: WaveformOptions;
|
|
97
|
+
|
|
98
|
+
/** Equalizer customization */
|
|
99
|
+
equalizerOptions?: EqualizerOptions;
|
|
100
|
+
|
|
101
|
+
/** Layout direction */
|
|
102
|
+
layout?: 'vertical' | 'horizontal';
|
|
103
|
+
|
|
104
|
+
/** Additional class name */
|
|
105
|
+
className?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// =============================================================================
|
|
109
|
+
// CONSTANTS
|
|
110
|
+
// =============================================================================
|
|
111
|
+
|
|
112
|
+
const COVER_SIZES = {
|
|
113
|
+
sm: 'w-24 h-24',
|
|
114
|
+
md: 'w-32 h-32',
|
|
115
|
+
lg: 'w-48 h-48',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// COMPONENT
|
|
120
|
+
// =============================================================================
|
|
121
|
+
|
|
122
|
+
export function SimpleAudioPlayer(props: SimpleAudioPlayerProps) {
|
|
123
|
+
return (
|
|
124
|
+
<VisualizationProvider>
|
|
125
|
+
<SimpleAudioPlayerContent {...props} />
|
|
126
|
+
</VisualizationProvider>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function SimpleAudioPlayerContent({
|
|
131
|
+
src,
|
|
132
|
+
title,
|
|
133
|
+
artist,
|
|
134
|
+
coverArt,
|
|
135
|
+
coverSize = 'md',
|
|
136
|
+
showWaveform = true,
|
|
137
|
+
showEqualizer = false,
|
|
138
|
+
showTimer = true,
|
|
139
|
+
showVolume = true,
|
|
140
|
+
reactiveCover = true,
|
|
141
|
+
variant,
|
|
142
|
+
intensity,
|
|
143
|
+
colorScheme,
|
|
144
|
+
autoPlay = false,
|
|
145
|
+
waveformOptions,
|
|
146
|
+
equalizerOptions,
|
|
147
|
+
layout = 'vertical',
|
|
148
|
+
className,
|
|
149
|
+
}: SimpleAudioPlayerProps) {
|
|
150
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
151
|
+
const { settings: vizSettings, nextVariant } = useAudioVisualization();
|
|
152
|
+
|
|
153
|
+
// Determine effective variant (from props or localStorage settings)
|
|
154
|
+
const effectiveVariant = variant ?? (vizSettings.variant !== 'none' ? vizSettings.variant : 'spotlight');
|
|
155
|
+
const effectiveIntensity = intensity ?? vizSettings.intensity;
|
|
156
|
+
const effectiveColorScheme = colorScheme ?? vizSettings.colorScheme;
|
|
157
|
+
|
|
158
|
+
// Show reactive cover if enabled and variant is not 'none'
|
|
159
|
+
const showReactiveCover = reactiveCover && effectiveVariant !== 'none';
|
|
160
|
+
|
|
161
|
+
// Render cover art content
|
|
162
|
+
const renderCoverContent = () => {
|
|
163
|
+
if (typeof coverArt === 'string') {
|
|
164
|
+
return (
|
|
165
|
+
<img
|
|
166
|
+
src={coverArt}
|
|
167
|
+
alt={title || 'Album cover'}
|
|
168
|
+
className="w-full h-full object-cover"
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (coverArt) {
|
|
174
|
+
return coverArt;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Default placeholder
|
|
178
|
+
return (
|
|
179
|
+
<div className="w-full h-full bg-muted/30 flex items-center justify-center">
|
|
180
|
+
<Music className="w-1/3 h-1/3 text-muted-foreground/50" />
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const isHorizontal = layout === 'horizontal';
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<AudioProvider
|
|
189
|
+
source={{ uri: src }}
|
|
190
|
+
containerRef={containerRef}
|
|
191
|
+
autoPlay={autoPlay}
|
|
192
|
+
waveformOptions={waveformOptions}
|
|
193
|
+
>
|
|
194
|
+
<div
|
|
195
|
+
className={cn(
|
|
196
|
+
'flex gap-4',
|
|
197
|
+
isHorizontal ? 'flex-row items-center' : 'flex-col items-center',
|
|
198
|
+
className
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
201
|
+
{/* Cover Art */}
|
|
202
|
+
{(coverArt || reactiveCover) && (
|
|
203
|
+
<div className="flex flex-col items-center gap-2 shrink-0">
|
|
204
|
+
{showReactiveCover ? (
|
|
205
|
+
<AudioReactiveCover
|
|
206
|
+
size={coverSize}
|
|
207
|
+
variant={effectiveVariant as 'glow' | 'orbs' | 'spotlight' | 'mesh'}
|
|
208
|
+
intensity={effectiveIntensity}
|
|
209
|
+
colorScheme={effectiveColorScheme}
|
|
210
|
+
onClick={nextVariant}
|
|
211
|
+
>
|
|
212
|
+
<div className={cn('rounded-lg overflow-hidden', COVER_SIZES[coverSize])}>
|
|
213
|
+
{renderCoverContent()}
|
|
214
|
+
</div>
|
|
215
|
+
</AudioReactiveCover>
|
|
216
|
+
) : (
|
|
217
|
+
<div
|
|
218
|
+
className={cn(
|
|
219
|
+
'rounded-lg overflow-hidden shadow-lg cursor-pointer',
|
|
220
|
+
COVER_SIZES[coverSize]
|
|
221
|
+
)}
|
|
222
|
+
onClick={nextVariant}
|
|
223
|
+
role="button"
|
|
224
|
+
tabIndex={0}
|
|
225
|
+
onKeyDown={(e) => e.key === 'Enter' && nextVariant()}
|
|
226
|
+
>
|
|
227
|
+
{renderCoverContent()}
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{/* Effect indicator */}
|
|
232
|
+
{reactiveCover && (
|
|
233
|
+
<span className="text-[10px] uppercase tracking-wider text-muted-foreground/50 select-none">
|
|
234
|
+
{vizSettings.variant === 'none' ? 'off' : vizSettings.variant}
|
|
235
|
+
</span>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{/* Track Info + Player */}
|
|
241
|
+
<div className={cn('flex flex-col gap-3', isHorizontal ? 'flex-1 min-w-0' : 'w-full max-w-md')}>
|
|
242
|
+
{/* Track Info */}
|
|
243
|
+
{(title || artist) && (
|
|
244
|
+
<div className={cn('text-center', isHorizontal && 'text-left')}>
|
|
245
|
+
{title && (
|
|
246
|
+
<h3 className="text-base font-medium text-foreground truncate">
|
|
247
|
+
{title}
|
|
248
|
+
</h3>
|
|
249
|
+
)}
|
|
250
|
+
{artist && (
|
|
251
|
+
<p className="text-sm text-muted-foreground truncate">
|
|
252
|
+
{artist}
|
|
253
|
+
</p>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
|
|
258
|
+
{/* Audio Player */}
|
|
259
|
+
<AudioPlayer
|
|
260
|
+
ref={containerRef}
|
|
261
|
+
showControls
|
|
262
|
+
showWaveform={showWaveform}
|
|
263
|
+
showEqualizer={showEqualizer}
|
|
264
|
+
showTimer={showTimer}
|
|
265
|
+
showVolume={showVolume}
|
|
266
|
+
equalizerOptions={equalizerOptions}
|
|
267
|
+
className="border-0 bg-transparent"
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
</AudioProvider>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export default SimpleAudioPlayer;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* VisualizationToggle - Simple toggle for audio visualization settings
|
|
5
|
+
*
|
|
6
|
+
* Click: cycle through variants
|
|
7
|
+
* Long press / right click: opens menu (future)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Sparkles } from 'lucide-react';
|
|
11
|
+
import { Button, cn } from '@djangocfg/ui-nextjs';
|
|
12
|
+
import {
|
|
13
|
+
useAudioVisualization,
|
|
14
|
+
VARIANT_INFO,
|
|
15
|
+
type VisualizationVariant,
|
|
16
|
+
} from './useAudioVisualization';
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// TYPES
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
export interface VisualizationToggleProps {
|
|
23
|
+
/** Compact mode (icon only) */
|
|
24
|
+
compact?: boolean;
|
|
25
|
+
/** Additional class name */
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// COMPONENT
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
export function VisualizationToggle({
|
|
34
|
+
compact = false,
|
|
35
|
+
className,
|
|
36
|
+
}: VisualizationToggleProps) {
|
|
37
|
+
const { settings, nextVariant } = useAudioVisualization();
|
|
38
|
+
|
|
39
|
+
const currentInfo = VARIANT_INFO[settings.variant];
|
|
40
|
+
const isEnabled = settings.variant !== 'none';
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Button
|
|
44
|
+
variant={isEnabled ? 'secondary' : 'ghost'}
|
|
45
|
+
size={compact ? 'icon' : 'sm'}
|
|
46
|
+
className={cn(
|
|
47
|
+
'transition-all',
|
|
48
|
+
isEnabled && 'bg-primary/10 text-primary hover:bg-primary/20',
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
onClick={nextVariant}
|
|
52
|
+
title={`Visualization: ${currentInfo.label} (click to change)`}
|
|
53
|
+
>
|
|
54
|
+
<Sparkles
|
|
55
|
+
className={cn(
|
|
56
|
+
'h-4 w-4',
|
|
57
|
+
isEnabled && 'text-primary',
|
|
58
|
+
!compact && 'mr-1.5'
|
|
59
|
+
)}
|
|
60
|
+
/>
|
|
61
|
+
{!compact && (
|
|
62
|
+
<span className="text-xs">{currentInfo.label}</span>
|
|
63
|
+
)}
|
|
64
|
+
</Button>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default VisualizationToggle;
|