@djangocfg/ui-nextjs 2.1.83 → 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.
- package/package.json +4 -4
- package/src/tools/AudioPlayer/README.md +60 -166
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +0 -35
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +0 -11
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +5 -5
- package/src/tools/AudioPlayer/components/index.ts +4 -8
- package/src/tools/AudioPlayer/context/index.ts +1 -8
- package/src/tools/AudioPlayer/hooks/index.ts +6 -13
- package/src/tools/AudioPlayer/index.ts +25 -89
- package/src/tools/AudioPlayer/types/index.ts +10 -18
- package/src/tools/index.ts +51 -56
- package/src/tools/AudioPlayer/@refactoring3/00-IMPLEMENTATION-ROADMAP.md +0 -1146
- package/src/tools/AudioPlayer/@refactoring3/01-WAVESURFER-STREAMING-ANALYSIS.md +0 -611
- package/src/tools/AudioPlayer/@refactoring3/02-MEDIA-VIEWER-ANALYSIS.md +0 -560
- package/src/tools/AudioPlayer/@refactoring3/03-HYBRID-ARCHITECTURE-PROPOSAL.md +0 -769
- package/src/tools/AudioPlayer/@refactoring3/04-CRACKLING-ISSUE-DIAGNOSIS.md +0 -373
- package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +0 -200
- package/src/tools/AudioPlayer/components/AudioPlayer.tsx +0 -236
- package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +0 -99
- package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +0 -278
- package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +0 -64
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +0 -376
- package/src/tools/AudioPlayer/context/selectors.ts +0 -96
- package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +0 -110
- package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +0 -150
- package/src/tools/AudioPlayer/hooks/useAudioSource.ts +0 -155
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +0 -109
- package/src/tools/AudioPlayer/progressive/ProgressiveAudioPlayer.tsx +0 -303
- package/src/tools/AudioPlayer/progressive/WaveformCanvas.tsx +0 -381
- package/src/tools/AudioPlayer/progressive/index.ts +0 -40
- package/src/tools/AudioPlayer/progressive/peaks.ts +0 -234
- package/src/tools/AudioPlayer/progressive/types.ts +0 -179
- package/src/tools/AudioPlayer/progressive/useAudioElement.ts +0 -340
- package/src/tools/AudioPlayer/progressive/useProgressiveWaveform.ts +0 -267
- package/src/tools/AudioPlayer/types/audio.ts +0 -121
- package/src/tools/AudioPlayer/types/components.ts +0 -98
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* AudioPlayer - WaveSurfer-based player component (Legacy)
|
|
5
|
-
*
|
|
6
|
-
* @deprecated Consider using `HybridAudioPlayer` instead for better streaming
|
|
7
|
-
* support and guaranteed no audio crackling.
|
|
8
|
-
*
|
|
9
|
-
* Uses AudioContext for state management.
|
|
10
|
-
* Renders waveform (via container ref), controls, timer, and equalizer.
|
|
11
|
-
* Supports keyboard shortcuts for playback control.
|
|
12
|
-
*
|
|
13
|
-
* @see HybridAudioPlayer for the recommended alternative
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { forwardRef } from 'react';
|
|
17
|
-
import {
|
|
18
|
-
Play,
|
|
19
|
-
Pause,
|
|
20
|
-
RotateCcw,
|
|
21
|
-
SkipBack,
|
|
22
|
-
SkipForward,
|
|
23
|
-
Volume2,
|
|
24
|
-
VolumeX,
|
|
25
|
-
Loader2,
|
|
26
|
-
Repeat,
|
|
27
|
-
} from 'lucide-react';
|
|
28
|
-
import { Button, Slider, cn } from '@djangocfg/ui-nextjs';
|
|
29
|
-
import { useAudio } from '../context';
|
|
30
|
-
import { AudioEqualizer } from './AudioEqualizer';
|
|
31
|
-
import { useAudioHotkeys } from '../hooks';
|
|
32
|
-
import { AudioShortcutsPopover } from './AudioShortcutsPopover';
|
|
33
|
-
import { formatTime } from '../utils';
|
|
34
|
-
import type { AudioPlayerProps } from '../types';
|
|
35
|
-
|
|
36
|
-
// =============================================================================
|
|
37
|
-
// COMPONENT
|
|
38
|
-
// =============================================================================
|
|
39
|
-
|
|
40
|
-
export const AudioPlayer = forwardRef<HTMLDivElement, AudioPlayerProps>(
|
|
41
|
-
function AudioPlayer(
|
|
42
|
-
{
|
|
43
|
-
showControls = true,
|
|
44
|
-
showWaveform = true,
|
|
45
|
-
showEqualizer = false,
|
|
46
|
-
showTimer = true,
|
|
47
|
-
showVolume = true,
|
|
48
|
-
showLoop = true,
|
|
49
|
-
equalizerOptions = {},
|
|
50
|
-
className,
|
|
51
|
-
style,
|
|
52
|
-
},
|
|
53
|
-
ref
|
|
54
|
-
) {
|
|
55
|
-
// Get all state and controls from context
|
|
56
|
-
const {
|
|
57
|
-
isReady,
|
|
58
|
-
isPlaying,
|
|
59
|
-
currentTime,
|
|
60
|
-
duration,
|
|
61
|
-
volume,
|
|
62
|
-
isMuted,
|
|
63
|
-
isLooping,
|
|
64
|
-
togglePlay,
|
|
65
|
-
restart,
|
|
66
|
-
skip,
|
|
67
|
-
setVolume,
|
|
68
|
-
toggleMute,
|
|
69
|
-
toggleLoop,
|
|
70
|
-
} = useAudio();
|
|
71
|
-
|
|
72
|
-
// Enable keyboard shortcuts
|
|
73
|
-
useAudioHotkeys({ enabled: isReady });
|
|
74
|
-
|
|
75
|
-
const isLoading = !isReady;
|
|
76
|
-
|
|
77
|
-
const handleVolumeChange = (value: number[]) => {
|
|
78
|
-
setVolume(value[0] / 100);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<div
|
|
83
|
-
className={cn(
|
|
84
|
-
'flex flex-col gap-3 p-4 rounded-lg bg-card border',
|
|
85
|
-
className
|
|
86
|
-
)}
|
|
87
|
-
style={style}
|
|
88
|
-
>
|
|
89
|
-
{/* Waveform container - rendered by WaveSurfer via context */}
|
|
90
|
-
{showWaveform && (
|
|
91
|
-
<div className="relative">
|
|
92
|
-
<div
|
|
93
|
-
ref={ref}
|
|
94
|
-
className={cn(
|
|
95
|
-
'w-full rounded transition-opacity',
|
|
96
|
-
isLoading && 'opacity-50'
|
|
97
|
-
)}
|
|
98
|
-
/>
|
|
99
|
-
{isLoading && (
|
|
100
|
-
<div className="absolute inset-0 flex items-center justify-center">
|
|
101
|
-
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
102
|
-
</div>
|
|
103
|
-
)}
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
|
|
107
|
-
{/* Equalizer animation */}
|
|
108
|
-
{showEqualizer && (
|
|
109
|
-
<AudioEqualizer
|
|
110
|
-
barCount={equalizerOptions.barCount}
|
|
111
|
-
height={equalizerOptions.height}
|
|
112
|
-
gap={equalizerOptions.gap}
|
|
113
|
-
showPeaks={equalizerOptions.showPeaks}
|
|
114
|
-
barColor={equalizerOptions.barColor}
|
|
115
|
-
peakColor={equalizerOptions.peakColor}
|
|
116
|
-
className="px-1"
|
|
117
|
-
/>
|
|
118
|
-
)}
|
|
119
|
-
|
|
120
|
-
{/* Timer */}
|
|
121
|
-
{showTimer && (
|
|
122
|
-
<div className="flex justify-between text-xs text-muted-foreground tabular-nums px-1">
|
|
123
|
-
<span>{formatTime(currentTime)}</span>
|
|
124
|
-
<span>{formatTime(duration)}</span>
|
|
125
|
-
</div>
|
|
126
|
-
)}
|
|
127
|
-
|
|
128
|
-
{/* Controls */}
|
|
129
|
-
{showControls && (
|
|
130
|
-
<div className="flex items-center justify-center gap-1">
|
|
131
|
-
{/* Restart */}
|
|
132
|
-
<Button
|
|
133
|
-
variant="ghost"
|
|
134
|
-
size="icon"
|
|
135
|
-
className="h-9 w-9"
|
|
136
|
-
onClick={restart}
|
|
137
|
-
disabled={!isReady}
|
|
138
|
-
title="Restart"
|
|
139
|
-
>
|
|
140
|
-
<RotateCcw className="h-4 w-4" />
|
|
141
|
-
</Button>
|
|
142
|
-
|
|
143
|
-
{/* Skip back 5s */}
|
|
144
|
-
<Button
|
|
145
|
-
variant="ghost"
|
|
146
|
-
size="icon"
|
|
147
|
-
className="h-9 w-9"
|
|
148
|
-
onClick={() => skip(-5)}
|
|
149
|
-
disabled={!isReady}
|
|
150
|
-
title="Back 5 seconds"
|
|
151
|
-
>
|
|
152
|
-
<SkipBack className="h-4 w-4" />
|
|
153
|
-
</Button>
|
|
154
|
-
|
|
155
|
-
{/* Play/Pause */}
|
|
156
|
-
<Button
|
|
157
|
-
variant="default"
|
|
158
|
-
size="icon"
|
|
159
|
-
className="h-12 w-12 rounded-full"
|
|
160
|
-
onClick={togglePlay}
|
|
161
|
-
disabled={!isReady && !isLoading}
|
|
162
|
-
title={isPlaying ? 'Pause' : 'Play'}
|
|
163
|
-
>
|
|
164
|
-
{isLoading ? (
|
|
165
|
-
<Loader2 className="h-5 w-5 animate-spin" />
|
|
166
|
-
) : isPlaying ? (
|
|
167
|
-
<Pause className="h-5 w-5" />
|
|
168
|
-
) : (
|
|
169
|
-
<Play className="h-5 w-5 ml-0.5" />
|
|
170
|
-
)}
|
|
171
|
-
</Button>
|
|
172
|
-
|
|
173
|
-
{/* Skip forward 5s */}
|
|
174
|
-
<Button
|
|
175
|
-
variant="ghost"
|
|
176
|
-
size="icon"
|
|
177
|
-
className="h-9 w-9"
|
|
178
|
-
onClick={() => skip(5)}
|
|
179
|
-
disabled={!isReady}
|
|
180
|
-
title="Forward 5 seconds"
|
|
181
|
-
>
|
|
182
|
-
<SkipForward className="h-4 w-4" />
|
|
183
|
-
</Button>
|
|
184
|
-
|
|
185
|
-
{/* Volume */}
|
|
186
|
-
{showVolume && (
|
|
187
|
-
<>
|
|
188
|
-
<Button
|
|
189
|
-
variant="ghost"
|
|
190
|
-
size="icon"
|
|
191
|
-
className="h-9 w-9"
|
|
192
|
-
onClick={toggleMute}
|
|
193
|
-
title={isMuted ? 'Unmute' : 'Mute'}
|
|
194
|
-
>
|
|
195
|
-
{isMuted || volume === 0 ? (
|
|
196
|
-
<VolumeX className="h-4 w-4" />
|
|
197
|
-
) : (
|
|
198
|
-
<Volume2 className="h-4 w-4" />
|
|
199
|
-
)}
|
|
200
|
-
</Button>
|
|
201
|
-
|
|
202
|
-
<Slider
|
|
203
|
-
value={[isMuted ? 0 : volume * 100]}
|
|
204
|
-
max={100}
|
|
205
|
-
step={1}
|
|
206
|
-
onValueChange={handleVolumeChange}
|
|
207
|
-
className="w-20"
|
|
208
|
-
aria-label="Volume"
|
|
209
|
-
/>
|
|
210
|
-
</>
|
|
211
|
-
)}
|
|
212
|
-
|
|
213
|
-
{/* Loop/Repeat */}
|
|
214
|
-
{showLoop && (
|
|
215
|
-
<Button
|
|
216
|
-
variant="ghost"
|
|
217
|
-
size="icon"
|
|
218
|
-
className={cn('h-9 w-9', isLooping && 'text-primary')}
|
|
219
|
-
onClick={toggleLoop}
|
|
220
|
-
disabled={!isReady}
|
|
221
|
-
title={isLooping ? 'Disable loop' : 'Enable loop'}
|
|
222
|
-
>
|
|
223
|
-
<Repeat className="h-4 w-4" />
|
|
224
|
-
</Button>
|
|
225
|
-
)}
|
|
226
|
-
|
|
227
|
-
{/* Shortcuts help */}
|
|
228
|
-
<AudioShortcutsPopover compact />
|
|
229
|
-
</div>
|
|
230
|
-
)}
|
|
231
|
-
</div>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
export default AudioPlayer;
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* AudioShortcutsPopover
|
|
5
|
-
*
|
|
6
|
-
* Displays keyboard shortcuts for audio player controls.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
Popover,
|
|
11
|
-
PopoverContent,
|
|
12
|
-
PopoverTrigger,
|
|
13
|
-
Button,
|
|
14
|
-
Kbd,
|
|
15
|
-
KbdGroup,
|
|
16
|
-
Separator,
|
|
17
|
-
Tooltip,
|
|
18
|
-
TooltipTrigger,
|
|
19
|
-
TooltipContent,
|
|
20
|
-
} from '@djangocfg/ui-nextjs';
|
|
21
|
-
import { Keyboard } from 'lucide-react';
|
|
22
|
-
import { AUDIO_SHORTCUTS } from '../hooks';
|
|
23
|
-
|
|
24
|
-
// =============================================================================
|
|
25
|
-
// TYPES
|
|
26
|
-
// =============================================================================
|
|
27
|
-
|
|
28
|
-
interface AudioShortcutsPopoverProps {
|
|
29
|
-
/** Compact mode for smaller displays */
|
|
30
|
-
compact?: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// =============================================================================
|
|
34
|
-
// COMPONENT
|
|
35
|
-
// =============================================================================
|
|
36
|
-
|
|
37
|
-
export function AudioShortcutsPopover({ compact = false }: AudioShortcutsPopoverProps) {
|
|
38
|
-
const trigger = (
|
|
39
|
-
<PopoverTrigger asChild>
|
|
40
|
-
<Button
|
|
41
|
-
variant="ghost"
|
|
42
|
-
size="icon"
|
|
43
|
-
className={compact ? 'h-6 w-6' : 'size-7 text-muted-foreground hover:text-foreground'}
|
|
44
|
-
title={compact ? undefined : 'Keyboard shortcuts'}
|
|
45
|
-
>
|
|
46
|
-
<Keyboard className={compact ? 'h-3.5 w-3.5' : 'size-4'} />
|
|
47
|
-
</Button>
|
|
48
|
-
</PopoverTrigger>
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<Popover>
|
|
53
|
-
{compact ? (
|
|
54
|
-
<Tooltip>
|
|
55
|
-
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
|
56
|
-
<TooltipContent side="bottom">Shortcuts</TooltipContent>
|
|
57
|
-
</Tooltip>
|
|
58
|
-
) : (
|
|
59
|
-
trigger
|
|
60
|
-
)}
|
|
61
|
-
<PopoverContent className="w-56 p-0" align="end">
|
|
62
|
-
<div className="px-3 py-2 border-b">
|
|
63
|
-
<h4 className="font-medium text-sm">Player Shortcuts</h4>
|
|
64
|
-
</div>
|
|
65
|
-
<div className="p-2 space-y-3 max-h-72 overflow-y-auto">
|
|
66
|
-
{AUDIO_SHORTCUTS.map((group, groupIndex) => (
|
|
67
|
-
<div key={group.title}>
|
|
68
|
-
{groupIndex > 0 && <Separator className="my-2" />}
|
|
69
|
-
<div className="text-xs font-medium text-muted-foreground mb-1.5 px-1">
|
|
70
|
-
{group.title}
|
|
71
|
-
</div>
|
|
72
|
-
<div className="space-y-1">
|
|
73
|
-
{group.shortcuts.map((shortcut) => (
|
|
74
|
-
<div
|
|
75
|
-
key={shortcut.label}
|
|
76
|
-
className="flex items-center justify-between px-1 py-0.5"
|
|
77
|
-
>
|
|
78
|
-
<span className="text-sm text-foreground">
|
|
79
|
-
{shortcut.label}
|
|
80
|
-
</span>
|
|
81
|
-
<KbdGroup>
|
|
82
|
-
{shortcut.keys.map((key, i) => (
|
|
83
|
-
<Kbd key={i} size="sm">
|
|
84
|
-
{key}
|
|
85
|
-
</Kbd>
|
|
86
|
-
))}
|
|
87
|
-
</KbdGroup>
|
|
88
|
-
</div>
|
|
89
|
-
))}
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
))}
|
|
93
|
-
</div>
|
|
94
|
-
</PopoverContent>
|
|
95
|
-
</Popover>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export default AudioShortcutsPopover;
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SimpleAudioPlayer - WaveSurfer-based audio player (Legacy)
|
|
5
|
-
*
|
|
6
|
-
* @deprecated Consider using `HybridSimplePlayer` instead for better streaming
|
|
7
|
-
* support, lower memory usage, and guaranteed no audio crackling.
|
|
8
|
-
*
|
|
9
|
-
* Use SimpleAudioPlayer only when you specifically need the static waveform
|
|
10
|
-
* visualization that shows the full audio amplitude shape.
|
|
11
|
-
*
|
|
12
|
-
* Migration:
|
|
13
|
-
* ```tsx
|
|
14
|
-
* // Before
|
|
15
|
-
* <SimpleAudioPlayer src={url} prefetch reactiveCover />
|
|
16
|
-
*
|
|
17
|
-
* // After
|
|
18
|
-
* <HybridSimplePlayer src={url} reactiveCover />
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* Key differences:
|
|
22
|
-
* - HybridSimplePlayer: Real-time frequency bars, native streaming, no prefetch needed
|
|
23
|
-
* - SimpleAudioPlayer: Static waveform shape, requires prefetch for streaming URLs
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { useRef, type ReactNode } from 'react';
|
|
27
|
-
import { Music } from 'lucide-react';
|
|
28
|
-
import { cn } from '@djangocfg/ui-core';
|
|
29
|
-
|
|
30
|
-
import { AudioProvider } from '../context';
|
|
31
|
-
import { AudioPlayer } from './AudioPlayer';
|
|
32
|
-
import { AudioReactiveCover } from './ReactiveCover';
|
|
33
|
-
import { VisualizationProvider, useVisualization } from '../hooks';
|
|
34
|
-
import type { WaveformOptions, EqualizerOptions } from '../types';
|
|
35
|
-
import type { EffectIntensity, EffectColorScheme } from '../effects';
|
|
36
|
-
import type { VisualizationVariant } from '../hooks';
|
|
37
|
-
|
|
38
|
-
// =============================================================================
|
|
39
|
-
// TYPES
|
|
40
|
-
// =============================================================================
|
|
41
|
-
|
|
42
|
-
export interface SimpleAudioPlayerProps {
|
|
43
|
-
/** Audio source URL */
|
|
44
|
-
src: string;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Pre-fetch audio as blob before loading into WaveSurfer.
|
|
48
|
-
* Required for streaming URLs because WaveSurfer needs complete file for seek to work.
|
|
49
|
-
* @default true
|
|
50
|
-
*/
|
|
51
|
-
prefetch?: boolean;
|
|
52
|
-
|
|
53
|
-
/** Track title */
|
|
54
|
-
title?: string;
|
|
55
|
-
|
|
56
|
-
/** Artist name */
|
|
57
|
-
artist?: string;
|
|
58
|
-
|
|
59
|
-
/** Cover art URL or ReactNode */
|
|
60
|
-
coverArt?: string | ReactNode;
|
|
61
|
-
|
|
62
|
-
/** Cover art size */
|
|
63
|
-
coverSize?: 'sm' | 'md' | 'lg';
|
|
64
|
-
|
|
65
|
-
/** Show waveform visualization */
|
|
66
|
-
showWaveform?: boolean;
|
|
67
|
-
|
|
68
|
-
/** Show equalizer bars */
|
|
69
|
-
showEqualizer?: boolean;
|
|
70
|
-
|
|
71
|
-
/** Show timer */
|
|
72
|
-
showTimer?: boolean;
|
|
73
|
-
|
|
74
|
-
/** Show volume control */
|
|
75
|
-
showVolume?: boolean;
|
|
76
|
-
|
|
77
|
-
/** Show loop/repeat button */
|
|
78
|
-
showLoop?: 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
|
-
prefetch = true,
|
|
133
|
-
title,
|
|
134
|
-
artist,
|
|
135
|
-
coverArt,
|
|
136
|
-
coverSize = 'md',
|
|
137
|
-
showWaveform = true,
|
|
138
|
-
showEqualizer = false,
|
|
139
|
-
showTimer = true,
|
|
140
|
-
showVolume = true,
|
|
141
|
-
showLoop = true,
|
|
142
|
-
reactiveCover = true,
|
|
143
|
-
variant,
|
|
144
|
-
intensity,
|
|
145
|
-
colorScheme,
|
|
146
|
-
autoPlay = false,
|
|
147
|
-
waveformOptions,
|
|
148
|
-
equalizerOptions,
|
|
149
|
-
layout = 'vertical',
|
|
150
|
-
className,
|
|
151
|
-
}: SimpleAudioPlayerProps) {
|
|
152
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
153
|
-
const { settings: vizSettings, nextVariant } = useVisualization();
|
|
154
|
-
|
|
155
|
-
// Determine effective variant (from props or localStorage settings)
|
|
156
|
-
const effectiveVariant = variant ?? (vizSettings.variant !== 'none' ? vizSettings.variant : 'spotlight');
|
|
157
|
-
const effectiveIntensity = intensity ?? vizSettings.intensity;
|
|
158
|
-
const effectiveColorScheme = colorScheme ?? vizSettings.colorScheme;
|
|
159
|
-
|
|
160
|
-
// Show reactive cover if enabled and variant is not 'none'
|
|
161
|
-
const showReactiveCover = reactiveCover && effectiveVariant !== 'none';
|
|
162
|
-
|
|
163
|
-
// Render cover art content
|
|
164
|
-
const renderCoverContent = () => {
|
|
165
|
-
if (typeof coverArt === 'string') {
|
|
166
|
-
return (
|
|
167
|
-
<img
|
|
168
|
-
src={coverArt}
|
|
169
|
-
alt={title || 'Album cover'}
|
|
170
|
-
className="w-full h-full object-cover"
|
|
171
|
-
/>
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (coverArt) {
|
|
176
|
-
return coverArt;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Default placeholder
|
|
180
|
-
return (
|
|
181
|
-
<div className="w-full h-full bg-muted/30 flex items-center justify-center">
|
|
182
|
-
<Music className="w-1/3 h-1/3 text-muted-foreground/50" />
|
|
183
|
-
</div>
|
|
184
|
-
);
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const isHorizontal = layout === 'horizontal';
|
|
188
|
-
|
|
189
|
-
return (
|
|
190
|
-
<AudioProvider
|
|
191
|
-
source={{ uri: src, prefetch }}
|
|
192
|
-
containerRef={containerRef}
|
|
193
|
-
autoPlay={autoPlay}
|
|
194
|
-
waveformOptions={waveformOptions}
|
|
195
|
-
>
|
|
196
|
-
<div
|
|
197
|
-
className={cn(
|
|
198
|
-
'flex gap-4',
|
|
199
|
-
isHorizontal ? 'flex-row items-center' : 'flex-col items-center',
|
|
200
|
-
className
|
|
201
|
-
)}
|
|
202
|
-
>
|
|
203
|
-
{/* Cover Art */}
|
|
204
|
-
{(coverArt || reactiveCover) && (
|
|
205
|
-
<div className="flex flex-col items-center gap-2 shrink-0">
|
|
206
|
-
{showReactiveCover ? (
|
|
207
|
-
<AudioReactiveCover
|
|
208
|
-
size={coverSize}
|
|
209
|
-
variant={effectiveVariant as 'glow' | 'orbs' | 'spotlight' | 'mesh'}
|
|
210
|
-
intensity={effectiveIntensity}
|
|
211
|
-
colorScheme={effectiveColorScheme}
|
|
212
|
-
onClick={nextVariant}
|
|
213
|
-
>
|
|
214
|
-
<div className={cn('rounded-lg overflow-hidden', COVER_SIZES[coverSize])}>
|
|
215
|
-
{renderCoverContent()}
|
|
216
|
-
</div>
|
|
217
|
-
</AudioReactiveCover>
|
|
218
|
-
) : (
|
|
219
|
-
<div
|
|
220
|
-
className={cn(
|
|
221
|
-
'rounded-lg overflow-hidden shadow-lg cursor-pointer',
|
|
222
|
-
COVER_SIZES[coverSize]
|
|
223
|
-
)}
|
|
224
|
-
onClick={nextVariant}
|
|
225
|
-
role="button"
|
|
226
|
-
tabIndex={0}
|
|
227
|
-
onKeyDown={(e) => e.key === 'Enter' && nextVariant()}
|
|
228
|
-
>
|
|
229
|
-
{renderCoverContent()}
|
|
230
|
-
</div>
|
|
231
|
-
)}
|
|
232
|
-
|
|
233
|
-
{/* Effect indicator */}
|
|
234
|
-
{reactiveCover && (
|
|
235
|
-
<span className="text-[10px] uppercase tracking-wider text-muted-foreground/50 select-none">
|
|
236
|
-
{vizSettings.variant === 'none' ? 'off' : vizSettings.variant}
|
|
237
|
-
</span>
|
|
238
|
-
)}
|
|
239
|
-
</div>
|
|
240
|
-
)}
|
|
241
|
-
|
|
242
|
-
{/* Track Info + Player */}
|
|
243
|
-
<div className={cn('flex flex-col gap-3', isHorizontal ? 'flex-1 min-w-0' : 'w-full max-w-md')}>
|
|
244
|
-
{/* Track Info */}
|
|
245
|
-
{(title || artist) && (
|
|
246
|
-
<div className={cn('text-center', isHorizontal && 'text-left')}>
|
|
247
|
-
{title && (
|
|
248
|
-
<h3 className="text-base font-medium text-foreground truncate">
|
|
249
|
-
{title}
|
|
250
|
-
</h3>
|
|
251
|
-
)}
|
|
252
|
-
{artist && (
|
|
253
|
-
<p className="text-sm text-muted-foreground truncate">
|
|
254
|
-
{artist}
|
|
255
|
-
</p>
|
|
256
|
-
)}
|
|
257
|
-
</div>
|
|
258
|
-
)}
|
|
259
|
-
|
|
260
|
-
{/* Audio Player */}
|
|
261
|
-
<AudioPlayer
|
|
262
|
-
ref={containerRef}
|
|
263
|
-
showControls
|
|
264
|
-
showWaveform={showWaveform}
|
|
265
|
-
showEqualizer={showEqualizer}
|
|
266
|
-
showTimer={showTimer}
|
|
267
|
-
showVolume={showVolume}
|
|
268
|
-
showLoop={showLoop}
|
|
269
|
-
equalizerOptions={equalizerOptions}
|
|
270
|
-
className="border-0 bg-transparent"
|
|
271
|
-
/>
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
274
|
-
</AudioProvider>
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export default SimpleAudioPlayer;
|
|
@@ -1,64 +0,0 @@
|
|
|
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 { useVisualization, VARIANT_INFO } from '../hooks';
|
|
13
|
-
|
|
14
|
-
// =============================================================================
|
|
15
|
-
// TYPES
|
|
16
|
-
// =============================================================================
|
|
17
|
-
|
|
18
|
-
export interface VisualizationToggleProps {
|
|
19
|
-
/** Compact mode (icon only) */
|
|
20
|
-
compact?: boolean;
|
|
21
|
-
/** Additional class name */
|
|
22
|
-
className?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// =============================================================================
|
|
26
|
-
// COMPONENT
|
|
27
|
-
// =============================================================================
|
|
28
|
-
|
|
29
|
-
export function VisualizationToggle({
|
|
30
|
-
compact = false,
|
|
31
|
-
className,
|
|
32
|
-
}: VisualizationToggleProps) {
|
|
33
|
-
const { settings, nextVariant } = useVisualization();
|
|
34
|
-
|
|
35
|
-
const currentInfo = VARIANT_INFO[settings.variant];
|
|
36
|
-
const isEnabled = settings.variant !== 'none';
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<Button
|
|
40
|
-
variant={isEnabled ? 'secondary' : 'ghost'}
|
|
41
|
-
size={compact ? 'icon' : 'sm'}
|
|
42
|
-
className={cn(
|
|
43
|
-
'transition-all',
|
|
44
|
-
isEnabled && 'bg-primary/10 text-primary hover:bg-primary/20',
|
|
45
|
-
className
|
|
46
|
-
)}
|
|
47
|
-
onClick={nextVariant}
|
|
48
|
-
title={`Visualization: ${currentInfo.label} (click to change)`}
|
|
49
|
-
>
|
|
50
|
-
<Sparkles
|
|
51
|
-
className={cn(
|
|
52
|
-
'h-4 w-4',
|
|
53
|
-
isEnabled && 'text-primary',
|
|
54
|
-
!compact && 'mr-1.5'
|
|
55
|
-
)}
|
|
56
|
-
/>
|
|
57
|
-
{!compact && (
|
|
58
|
-
<span className="text-xs">{currentInfo.label}</span>
|
|
59
|
-
)}
|
|
60
|
-
</Button>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export default VisualizationToggle;
|