@djangocfg/ui-nextjs 2.1.65 → 2.1.67
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 +13 -8
- package/src/blocks/SplitHero/SplitHeroMedia.tsx +2 -1
- package/src/stores/index.ts +8 -0
- package/src/stores/mediaCache.ts +464 -0
- package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
- package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
- package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
- package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
- package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
- package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
- package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
- package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
- package/src/tools/AudioPlayer/README.md +325 -0
- package/src/tools/AudioPlayer/components/AudioEqualizer.tsx +200 -0
- package/src/tools/AudioPlayer/components/AudioPlayer.tsx +231 -0
- package/src/tools/AudioPlayer/components/AudioShortcutsPopover.tsx +99 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
- package/src/tools/AudioPlayer/components/SimpleAudioPlayer.tsx +280 -0
- package/src/tools/AudioPlayer/components/VisualizationToggle.tsx +64 -0
- package/src/tools/AudioPlayer/components/index.ts +21 -0
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
- package/src/tools/AudioPlayer/context/index.ts +11 -0
- package/src/tools/AudioPlayer/context/selectors.ts +96 -0
- package/src/tools/AudioPlayer/effects/index.ts +412 -0
- package/src/tools/AudioPlayer/hooks/index.ts +29 -0
- package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
- package/src/tools/AudioPlayer/hooks/useAudioHotkeys.ts +149 -0
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +201 -0
- package/src/tools/AudioPlayer/index.ts +139 -0
- package/src/tools/AudioPlayer/types/audio.ts +107 -0
- package/src/tools/AudioPlayer/types/components.ts +98 -0
- package/src/tools/AudioPlayer/types/effects.ts +73 -0
- package/src/tools/AudioPlayer/types/index.ts +35 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
- package/src/tools/AudioPlayer/utils/index.ts +5 -0
- package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
- package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
- package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
- package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
- package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
- package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
- package/src/tools/ImageViewer/README.md +174 -0
- package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
- package/src/tools/ImageViewer/components/index.ts +7 -0
- package/src/tools/ImageViewer/hooks/index.ts +9 -0
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
- package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
- package/src/tools/ImageViewer/index.ts +60 -0
- package/src/tools/ImageViewer/types.ts +75 -0
- package/src/tools/ImageViewer/utils/constants.ts +59 -0
- package/src/tools/ImageViewer/utils/index.ts +16 -0
- package/src/tools/ImageViewer/utils/lqip.ts +47 -0
- package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
- package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
- package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
- package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
- package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
- package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
- package/src/tools/VideoPlayer/README.md +212 -187
- package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +174 -0
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
- package/src/tools/VideoPlayer/components/index.ts +14 -0
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
- package/src/tools/VideoPlayer/context/index.ts +8 -0
- package/src/tools/VideoPlayer/hooks/index.ts +9 -0
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
- package/src/tools/VideoPlayer/index.ts +70 -9
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +206 -0
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +401 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +332 -0
- package/src/tools/VideoPlayer/providers/index.ts +8 -0
- package/src/tools/VideoPlayer/types/index.ts +38 -0
- package/src/tools/VideoPlayer/types/player.ts +116 -0
- package/src/tools/VideoPlayer/types/provider.ts +93 -0
- package/src/tools/VideoPlayer/types/sources.ts +97 -0
- package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
- package/src/tools/VideoPlayer/utils/index.ts +11 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
- package/src/tools/index.ts +92 -4
- package/src/tools/VideoPlayer/NativePlayer.tsx +0 -141
- package/src/tools/VideoPlayer/VideoPlayer.tsx +0 -231
- package/src/tools/VideoPlayer/types.ts +0 -118
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Execution Checklist
|
|
2
|
+
|
|
3
|
+
## Pre-flight
|
|
4
|
+
|
|
5
|
+
- [ ] Run `pnpm check` - ensure current code compiles
|
|
6
|
+
- [ ] Commit current state as backup
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Phase 1: Create Folder Structure
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cd src/tools/AudioPlayer
|
|
14
|
+
mkdir -p types hooks context components/ReactiveCover effects utils
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- [ ] Create `types/` folder
|
|
18
|
+
- [ ] Create `hooks/` folder
|
|
19
|
+
- [ ] Create `context/` folder
|
|
20
|
+
- [ ] Create `components/` folder
|
|
21
|
+
- [ ] Create `components/ReactiveCover/` folder
|
|
22
|
+
- [ ] Create `effects/` subfolder (already exists, will add files)
|
|
23
|
+
- [ ] Create `utils/` folder
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Phase 2: Types Split
|
|
28
|
+
|
|
29
|
+
- [ ] Create `types/audio.ts`
|
|
30
|
+
- [ ] Create `types/components.ts`
|
|
31
|
+
- [ ] Create `types/effects.ts`
|
|
32
|
+
- [ ] Create `types/index.ts`
|
|
33
|
+
- [ ] Run `pnpm check`
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Phase 3: Hooks Extraction
|
|
38
|
+
|
|
39
|
+
- [ ] Create `hooks/useSharedWebAudio.ts` (extract from context.tsx)
|
|
40
|
+
- [ ] Create `hooks/useAudioAnalysis.ts` (extract from context.tsx)
|
|
41
|
+
- [ ] Move `useAudioHotkeys.ts` → `hooks/useAudioHotkeys.ts`
|
|
42
|
+
- [ ] Move `useAudioVisualization.tsx` → `hooks/useVisualization.tsx`
|
|
43
|
+
- [ ] Create `hooks/index.ts`
|
|
44
|
+
- [ ] Run `pnpm check`
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Phase 4: Context Refactoring
|
|
49
|
+
|
|
50
|
+
- [ ] Create `context/AudioProvider.tsx`
|
|
51
|
+
- [ ] Create `context/selectors.ts`
|
|
52
|
+
- [ ] Create `context/index.ts`
|
|
53
|
+
- [ ] Delete old `context.tsx`
|
|
54
|
+
- [ ] Run `pnpm check`
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Phase 5: Components Reorganization
|
|
59
|
+
|
|
60
|
+
- [ ] Move `AudioPlayer.tsx` → `components/AudioPlayer.tsx`
|
|
61
|
+
- [ ] Move `AudioEqualizer.tsx` → `components/AudioEqualizer.tsx`
|
|
62
|
+
- [ ] Move `SimpleAudioPlayer.tsx` → `components/SimpleAudioPlayer.tsx`
|
|
63
|
+
- [ ] Move `AudioShortcutsPopover.tsx` → `components/ShortcutsPopover.tsx`
|
|
64
|
+
- [ ] Move `VisualizationToggle.tsx` → `components/VisualizationToggle.tsx`
|
|
65
|
+
- [ ] Split `AudioReactiveCover.tsx` into `components/ReactiveCover/`
|
|
66
|
+
- [ ] Create `components/ReactiveCover/index.tsx`
|
|
67
|
+
- [ ] Create `components/ReactiveCover/GlowEffect.tsx`
|
|
68
|
+
- [ ] Create `components/ReactiveCover/OrbsEffect.tsx`
|
|
69
|
+
- [ ] Create `components/ReactiveCover/SpotlightEffect.tsx`
|
|
70
|
+
- [ ] Create `components/ReactiveCover/MeshEffect.tsx`
|
|
71
|
+
- [ ] Create `components/index.ts`
|
|
72
|
+
- [ ] Delete old component files from root
|
|
73
|
+
- [ ] Run `pnpm check`
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Phase 6: Effects Refactoring
|
|
78
|
+
|
|
79
|
+
- [ ] Create `effects/constants.ts`
|
|
80
|
+
- [ ] Create `effects/calculations.ts`
|
|
81
|
+
- [ ] Create `effects/animations.ts`
|
|
82
|
+
- [ ] Update `effects/index.ts`
|
|
83
|
+
- [ ] Run `pnpm check`
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Phase 7: Utils
|
|
88
|
+
|
|
89
|
+
- [ ] Create `utils/formatTime.ts`
|
|
90
|
+
- [ ] Create `utils/index.ts`
|
|
91
|
+
- [ ] Run `pnpm check`
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Phase 8: Update Main Index
|
|
96
|
+
|
|
97
|
+
- [ ] Rewrite `index.ts` with new import paths
|
|
98
|
+
- [ ] Run `pnpm check`
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Phase 9: Cleanup & Verification
|
|
103
|
+
|
|
104
|
+
- [ ] Delete old files:
|
|
105
|
+
- [ ] `context.tsx`
|
|
106
|
+
- [ ] `types.ts`
|
|
107
|
+
- [ ] `useAudioHotkeys.ts` (moved)
|
|
108
|
+
- [ ] `useAudioVisualization.tsx` (moved)
|
|
109
|
+
- [ ] `AudioPlayer.tsx` (moved)
|
|
110
|
+
- [ ] `AudioEqualizer.tsx` (moved)
|
|
111
|
+
- [ ] `SimpleAudioPlayer.tsx` (moved)
|
|
112
|
+
- [ ] `AudioShortcutsPopover.tsx` (moved)
|
|
113
|
+
- [ ] `VisualizationToggle.tsx` (moved)
|
|
114
|
+
- [ ] `AudioReactiveCover.tsx` (split)
|
|
115
|
+
|
|
116
|
+
- [ ] Run `pnpm check` - final verification
|
|
117
|
+
- [ ] Test in playground
|
|
118
|
+
- [ ] Commit refactoring
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Rollback Plan
|
|
123
|
+
|
|
124
|
+
If something goes wrong:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
git checkout HEAD -- src/tools/AudioPlayer/
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Files to Delete (after successful migration)
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
src/tools/AudioPlayer/
|
|
136
|
+
├── context.tsx # → context/AudioProvider.tsx + context/selectors.ts
|
|
137
|
+
├── types.ts # → types/*.ts
|
|
138
|
+
├── useAudioHotkeys.ts # → hooks/useAudioHotkeys.ts
|
|
139
|
+
├── useAudioVisualization.tsx # → hooks/useVisualization.tsx
|
|
140
|
+
├── AudioPlayer.tsx # → components/AudioPlayer.tsx
|
|
141
|
+
├── AudioEqualizer.tsx # → components/AudioEqualizer.tsx
|
|
142
|
+
├── SimpleAudioPlayer.tsx # → components/SimpleAudioPlayer.tsx
|
|
143
|
+
├── AudioShortcutsPopover.tsx # → components/ShortcutsPopover.tsx
|
|
144
|
+
├── VisualizationToggle.tsx # → components/VisualizationToggle.tsx
|
|
145
|
+
└── AudioReactiveCover.tsx # → components/ReactiveCover/*.tsx
|
|
146
|
+
```
|
|
@@ -0,0 +1,325 @@
|
|
|
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 # Public API exports
|
|
290
|
+
├── types/
|
|
291
|
+
│ ├── index.ts # Type re-exports
|
|
292
|
+
│ ├── audio.ts # Audio state & source types
|
|
293
|
+
│ ├── components.ts # Component prop types
|
|
294
|
+
│ └── effects.ts # Visualization effect types
|
|
295
|
+
├── hooks/
|
|
296
|
+
│ ├── index.ts
|
|
297
|
+
│ ├── useAudioHotkeys.ts # Keyboard shortcuts
|
|
298
|
+
│ ├── useVisualization.tsx # Visualization settings
|
|
299
|
+
│ ├── useAudioAnalysis.ts # Web Audio frequency analysis
|
|
300
|
+
│ └── useSharedWebAudio.ts # Shared AudioContext
|
|
301
|
+
├── utils/
|
|
302
|
+
│ ├── index.ts
|
|
303
|
+
│ └── formatTime.ts # Time formatting
|
|
304
|
+
├── context/
|
|
305
|
+
│ ├── index.ts
|
|
306
|
+
│ ├── AudioProvider.tsx # Audio state provider
|
|
307
|
+
│ └── selectors.ts # useAudio, useAudioControls hooks
|
|
308
|
+
├── components/
|
|
309
|
+
│ ├── index.ts
|
|
310
|
+
│ ├── AudioPlayer.tsx # Main player component
|
|
311
|
+
│ ├── SimpleAudioPlayer.tsx # All-in-one wrapper
|
|
312
|
+
│ ├── AudioEqualizer.tsx # Frequency bars
|
|
313
|
+
│ ├── AudioShortcutsPopover.tsx # Shortcuts help
|
|
314
|
+
│ ├── VisualizationToggle.tsx # Effect toggle button
|
|
315
|
+
│ └── ReactiveCover/
|
|
316
|
+
│ ├── index.ts
|
|
317
|
+
│ ├── AudioReactiveCover.tsx # Reactive album art
|
|
318
|
+
│ └── effects/ # Effect components
|
|
319
|
+
│ ├── GlowEffect.tsx
|
|
320
|
+
│ ├── OrbsEffect.tsx
|
|
321
|
+
│ ├── SpotlightEffect.tsx
|
|
322
|
+
│ └── MeshEffect.tsx
|
|
323
|
+
└── effects/
|
|
324
|
+
└── index.ts # Effect calculations
|
|
325
|
+
```
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AudioEqualizer - Real-time frequency visualizer with animated bars
|
|
5
|
+
*
|
|
6
|
+
* Uses shared Web Audio context from AudioProvider for real-time frequency analysis.
|
|
7
|
+
* This prevents "InvalidStateError" from creating multiple MediaElementSourceNodes.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Real-time frequency visualization
|
|
11
|
+
* - Configurable number of bars
|
|
12
|
+
* - Theme-aware colors (dark/light support)
|
|
13
|
+
* - Smooth animations with CSS transitions
|
|
14
|
+
* - Peak hold indicators
|
|
15
|
+
* - Pre-allocated buffers for performance
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
19
|
+
import { cn } from '@djangocfg/ui-nextjs';
|
|
20
|
+
import { useAudioElement } from '../context';
|
|
21
|
+
import type { AudioEqualizerProps } from '../types';
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// CONSTANTS
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
const DEFAULT_BAR_COUNT = 24;
|
|
28
|
+
const DEFAULT_HEIGHT = 48;
|
|
29
|
+
const DEFAULT_GAP = 2;
|
|
30
|
+
const PEAK_DECAY_RATE = 0.02;
|
|
31
|
+
const PEAK_HOLD_TIME = 500;
|
|
32
|
+
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// COMPONENT
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
export function AudioEqualizer({
|
|
38
|
+
barCount = DEFAULT_BAR_COUNT,
|
|
39
|
+
height = DEFAULT_HEIGHT,
|
|
40
|
+
gap = DEFAULT_GAP,
|
|
41
|
+
showPeaks = true,
|
|
42
|
+
barColor,
|
|
43
|
+
peakColor,
|
|
44
|
+
className,
|
|
45
|
+
}: AudioEqualizerProps) {
|
|
46
|
+
// Get shared audio context from AudioProvider
|
|
47
|
+
const { sharedAudio, isPlaying } = useAudioElement();
|
|
48
|
+
|
|
49
|
+
const [frequencies, setFrequencies] = useState<number[]>(() =>
|
|
50
|
+
new Array(barCount).fill(0)
|
|
51
|
+
);
|
|
52
|
+
const [peaks, setPeaks] = useState<number[]>(() =>
|
|
53
|
+
new Array(barCount).fill(0)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Refs for analyzer and animation
|
|
57
|
+
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
58
|
+
const animationRef = useRef<number | null>(null);
|
|
59
|
+
const peakTimersRef = useRef<number[]>(new Array(barCount).fill(0));
|
|
60
|
+
// Pre-allocated buffer for frequency data (prevents allocation per frame)
|
|
61
|
+
const dataArrayRef = useRef<Uint8Array | null>(null);
|
|
62
|
+
|
|
63
|
+
// Cleanup function
|
|
64
|
+
const cleanup = useCallback(() => {
|
|
65
|
+
if (animationRef.current) {
|
|
66
|
+
cancelAnimationFrame(animationRef.current);
|
|
67
|
+
animationRef.current = null;
|
|
68
|
+
}
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
// Create analyzer using shared context
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
// Wait for shared audio to be ready
|
|
74
|
+
if (!sharedAudio.sourceNode) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Only create analyzer once
|
|
79
|
+
if (analyserRef.current) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const analyser = sharedAudio.createAnalyser({ fftSize: 64, smoothing: 0.8 });
|
|
84
|
+
if (analyser) {
|
|
85
|
+
analyserRef.current = analyser;
|
|
86
|
+
// Pre-allocate data array
|
|
87
|
+
dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
if (analyserRef.current) {
|
|
92
|
+
sharedAudio.disconnectAnalyser(analyserRef.current);
|
|
93
|
+
analyserRef.current = null;
|
|
94
|
+
dataArrayRef.current = null;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}, [sharedAudio.sourceNode, sharedAudio.createAnalyser, sharedAudio.disconnectAnalyser]);
|
|
98
|
+
|
|
99
|
+
// Animation loop
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!isPlaying || !analyserRef.current || !dataArrayRef.current) {
|
|
102
|
+
cleanup();
|
|
103
|
+
setFrequencies(new Array(barCount).fill(0));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const analyser = analyserRef.current;
|
|
108
|
+
const dataArray = dataArrayRef.current;
|
|
109
|
+
|
|
110
|
+
const animate = () => {
|
|
111
|
+
analyser.getByteFrequencyData(dataArray as Uint8Array<ArrayBuffer>);
|
|
112
|
+
|
|
113
|
+
const step = Math.floor(dataArray.length / barCount);
|
|
114
|
+
const newFrequencies: number[] = [];
|
|
115
|
+
const newPeaks: number[] = [...peaks];
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < barCount; i++) {
|
|
119
|
+
let sum = 0;
|
|
120
|
+
for (let j = 0; j < step; j++) {
|
|
121
|
+
sum += dataArray[i * step + j] || 0;
|
|
122
|
+
}
|
|
123
|
+
const value = sum / step / 255;
|
|
124
|
+
newFrequencies.push(value);
|
|
125
|
+
|
|
126
|
+
if (showPeaks) {
|
|
127
|
+
if (value > newPeaks[i]) {
|
|
128
|
+
newPeaks[i] = value;
|
|
129
|
+
peakTimersRef.current[i] = now;
|
|
130
|
+
} else if (now - peakTimersRef.current[i] > PEAK_HOLD_TIME) {
|
|
131
|
+
newPeaks[i] = Math.max(0, newPeaks[i] - PEAK_DECAY_RATE);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
setFrequencies(newFrequencies);
|
|
137
|
+
if (showPeaks) {
|
|
138
|
+
setPeaks(newPeaks);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
animationRef.current = requestAnimationFrame(animate);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
animationRef.current = requestAnimationFrame(animate);
|
|
145
|
+
|
|
146
|
+
return cleanup;
|
|
147
|
+
}, [isPlaying, barCount, showPeaks, cleanup, peaks]);
|
|
148
|
+
|
|
149
|
+
// Reset peaks array when barCount changes
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
setPeaks(new Array(barCount).fill(0));
|
|
152
|
+
peakTimersRef.current = new Array(barCount).fill(0);
|
|
153
|
+
}, [barCount]);
|
|
154
|
+
|
|
155
|
+
// Calculate bar width
|
|
156
|
+
const totalGaps = (barCount - 1) * gap;
|
|
157
|
+
const barWidth = `calc((100% - ${totalGaps}px) / ${barCount})`;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
className={cn('relative flex items-end justify-between', className)}
|
|
162
|
+
style={{ height }}
|
|
163
|
+
>
|
|
164
|
+
{frequencies.map((freq, index) => (
|
|
165
|
+
<div
|
|
166
|
+
key={index}
|
|
167
|
+
className="relative flex flex-col justify-end"
|
|
168
|
+
style={{
|
|
169
|
+
width: barWidth,
|
|
170
|
+
height: '100%',
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
{/* Peak indicator */}
|
|
174
|
+
{showPeaks && peaks[index] > 0.01 && (
|
|
175
|
+
<div
|
|
176
|
+
className="absolute w-full transition-all duration-75"
|
|
177
|
+
style={{
|
|
178
|
+
height: 2,
|
|
179
|
+
bottom: `${peaks[index] * 100}%`,
|
|
180
|
+
backgroundColor: peakColor || 'hsl(217 91% 70%)',
|
|
181
|
+
}}
|
|
182
|
+
/>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{/* Frequency bar */}
|
|
186
|
+
<div
|
|
187
|
+
className="w-full rounded-t-sm transition-all duration-75"
|
|
188
|
+
style={{
|
|
189
|
+
height: `${Math.max(freq * 100, 2)}%`,
|
|
190
|
+
backgroundColor: barColor || 'hsl(217 91% 60%)',
|
|
191
|
+
opacity: 0.3 + freq * 0.7,
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export default AudioEqualizer;
|