@djangocfg/ui-nextjs 2.1.66 → 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 +8 -6
- 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 +35 -11
- package/src/tools/AudioPlayer/{AudioEqualizer.tsx → components/AudioEqualizer.tsx} +29 -64
- package/src/tools/AudioPlayer/{AudioPlayer.tsx → components/AudioPlayer.tsx} +22 -14
- package/src/tools/AudioPlayer/{AudioShortcutsPopover.tsx → components/AudioShortcutsPopover.tsx} +6 -2
- 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/{SimpleAudioPlayer.tsx → components/SimpleAudioPlayer.tsx} +12 -7
- package/src/tools/AudioPlayer/{VisualizationToggle.tsx → components/VisualizationToggle.tsx} +2 -6
- 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/hooks/index.ts +29 -0
- package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
- package/src/tools/AudioPlayer/{useAudioHotkeys.ts → hooks/useAudioHotkeys.ts} +11 -4
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
- package/src/tools/AudioPlayer/{useAudioVisualization.tsx → hooks/useVisualization.tsx} +11 -5
- package/src/tools/AudioPlayer/index.ts +104 -49
- package/src/tools/AudioPlayer/types/audio.ts +107 -0
- package/src/tools/AudioPlayer/{types.ts → types/components.ts} +20 -84
- 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 +16 -3
- 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 +47 -3
- 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 +26 -10
- package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
- package/src/tools/VideoPlayer/{VideoErrorFallback.tsx → components/VideoErrorFallback.tsx} +2 -2
- package/src/tools/VideoPlayer/{VideoPlayer.tsx → components/VideoPlayer.tsx} +4 -5
- 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 +29 -20
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +118 -28
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +89 -11
- 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 +10 -0
- package/src/tools/AudioPlayer/AudioReactiveCover.tsx +0 -389
- package/src/tools/AudioPlayer/context.tsx +0 -426
- package/src/tools/ImageViewer/ImageViewer.tsx +0 -416
- package/src/tools/VideoPlayer/VideoPlayerContext.tsx +0 -125
- package/src/tools/VideoPlayer/types.ts +0 -367
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Phase 2: Types Split
|
|
2
|
+
|
|
3
|
+
## Source: `types.ts` (185 lines)
|
|
4
|
+
|
|
5
|
+
Split into 3 files by domain.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## `types/audio.ts`
|
|
10
|
+
|
|
11
|
+
Core audio-related types.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import type WaveSurfer from 'wavesurfer.js';
|
|
15
|
+
import type { AudioLevels } from '../effects';
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// AUDIO SOURCE
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
export interface AudioSource {
|
|
22
|
+
uri: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// PLAYBACK STATE
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
export interface PlaybackStatus {
|
|
30
|
+
isLoaded: boolean;
|
|
31
|
+
isPlaying: boolean;
|
|
32
|
+
duration: number;
|
|
33
|
+
currentTime: number;
|
|
34
|
+
volume: number;
|
|
35
|
+
isMuted: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// WAVEFORM OPTIONS
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
export interface WaveformOptions {
|
|
43
|
+
waveColor?: string;
|
|
44
|
+
progressColor?: string;
|
|
45
|
+
height?: number;
|
|
46
|
+
barWidth?: number;
|
|
47
|
+
barRadius?: number;
|
|
48
|
+
barGap?: number;
|
|
49
|
+
cursorWidth?: number;
|
|
50
|
+
cursorColor?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// EQUALIZER OPTIONS
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
export interface EqualizerOptions {
|
|
58
|
+
barCount?: number;
|
|
59
|
+
height?: number;
|
|
60
|
+
gap?: number;
|
|
61
|
+
showPeaks?: boolean;
|
|
62
|
+
barColor?: string;
|
|
63
|
+
peakColor?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// SHARED WEB AUDIO CONTEXT
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
export interface SharedWebAudioContext {
|
|
71
|
+
audioContext: AudioContext | null;
|
|
72
|
+
sourceNode: MediaElementAudioSourceNode | null;
|
|
73
|
+
createAnalyser: (options?: { fftSize?: number; smoothing?: number }) => AnalyserNode | null;
|
|
74
|
+
disconnectAnalyser: (analyser: AnalyserNode) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// AUDIO CONTEXT STATE
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
export interface AudioContextState {
|
|
82
|
+
// Core instances
|
|
83
|
+
wavesurfer: WaveSurfer | null;
|
|
84
|
+
audioElement: HTMLMediaElement | null;
|
|
85
|
+
sharedAudio: SharedWebAudioContext;
|
|
86
|
+
|
|
87
|
+
// Playback state
|
|
88
|
+
isReady: boolean;
|
|
89
|
+
isPlaying: boolean;
|
|
90
|
+
currentTime: number;
|
|
91
|
+
duration: number;
|
|
92
|
+
volume: number;
|
|
93
|
+
isMuted: boolean;
|
|
94
|
+
isLooping: boolean;
|
|
95
|
+
|
|
96
|
+
// Audio analysis
|
|
97
|
+
audioLevels: AudioLevels;
|
|
98
|
+
|
|
99
|
+
// Actions
|
|
100
|
+
play: () => Promise<void>;
|
|
101
|
+
pause: () => void;
|
|
102
|
+
togglePlay: () => void;
|
|
103
|
+
seek: (time: number) => void;
|
|
104
|
+
seekTo: (progress: number) => void;
|
|
105
|
+
skip: (seconds: number) => void;
|
|
106
|
+
setVolume: (volume: number) => void;
|
|
107
|
+
toggleMute: () => void;
|
|
108
|
+
toggleLoop: () => void;
|
|
109
|
+
setLoop: (enabled: boolean) => void;
|
|
110
|
+
restart: () => void;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## `types/components.ts`
|
|
117
|
+
|
|
118
|
+
Component props types.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import type { CSSProperties } from 'react';
|
|
122
|
+
import type { WaveformOptions, EqualizerOptions, AudioSource, PlaybackStatus } from './audio';
|
|
123
|
+
|
|
124
|
+
// =============================================================================
|
|
125
|
+
// AUDIO PLAYER PROPS
|
|
126
|
+
// =============================================================================
|
|
127
|
+
|
|
128
|
+
export interface AudioPlayerProps {
|
|
129
|
+
showControls?: boolean;
|
|
130
|
+
showWaveform?: boolean;
|
|
131
|
+
showEqualizer?: boolean;
|
|
132
|
+
showTimer?: boolean;
|
|
133
|
+
showVolume?: boolean;
|
|
134
|
+
showLoop?: boolean;
|
|
135
|
+
waveformOptions?: WaveformOptions;
|
|
136
|
+
equalizerOptions?: EqualizerOptions;
|
|
137
|
+
className?: string;
|
|
138
|
+
style?: CSSProperties;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// =============================================================================
|
|
142
|
+
// AUDIO EQUALIZER PROPS
|
|
143
|
+
// =============================================================================
|
|
144
|
+
|
|
145
|
+
export interface AudioEqualizerProps {
|
|
146
|
+
barCount?: number;
|
|
147
|
+
height?: number;
|
|
148
|
+
gap?: number;
|
|
149
|
+
showPeaks?: boolean;
|
|
150
|
+
barColor?: string;
|
|
151
|
+
peakColor?: string;
|
|
152
|
+
className?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// =============================================================================
|
|
156
|
+
// AUDIO REACTIVE COVER PROPS
|
|
157
|
+
// =============================================================================
|
|
158
|
+
|
|
159
|
+
export interface AudioReactiveCoverProps {
|
|
160
|
+
variant?: 'glow' | 'orbs' | 'spotlight' | 'mesh';
|
|
161
|
+
intensity?: 'subtle' | 'medium' | 'strong';
|
|
162
|
+
colorScheme?: 'primary' | 'vibrant' | 'cool' | 'warm';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// =============================================================================
|
|
166
|
+
// AUDIO VIEWER PROPS (legacy)
|
|
167
|
+
// =============================================================================
|
|
168
|
+
|
|
169
|
+
export interface AudioViewerProps {
|
|
170
|
+
source: AudioSource;
|
|
171
|
+
autoPlay?: boolean;
|
|
172
|
+
showControls?: boolean;
|
|
173
|
+
showWaveform?: boolean;
|
|
174
|
+
showEqualizer?: boolean;
|
|
175
|
+
showTimer?: boolean;
|
|
176
|
+
showVolume?: boolean;
|
|
177
|
+
waveformOptions?: WaveformOptions;
|
|
178
|
+
equalizerOptions?: EqualizerOptions;
|
|
179
|
+
onPlaybackStatusUpdate?: (status: PlaybackStatus) => void;
|
|
180
|
+
className?: string;
|
|
181
|
+
style?: CSSProperties;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## `types/effects.ts`
|
|
188
|
+
|
|
189
|
+
Effect-related types (moved from effects/index.ts).
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// =============================================================================
|
|
193
|
+
// EFFECT TYPES
|
|
194
|
+
// =============================================================================
|
|
195
|
+
|
|
196
|
+
export type EffectVariant = 'glow' | 'orbs' | 'spotlight' | 'mesh';
|
|
197
|
+
export type EffectIntensity = 'subtle' | 'medium' | 'strong';
|
|
198
|
+
export type EffectColorScheme = 'primary' | 'vibrant' | 'cool' | 'warm';
|
|
199
|
+
|
|
200
|
+
export interface AudioLevels {
|
|
201
|
+
bass: number;
|
|
202
|
+
mid: number;
|
|
203
|
+
high: number;
|
|
204
|
+
overall: number;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface EffectConfig {
|
|
208
|
+
opacity: number;
|
|
209
|
+
scale: number;
|
|
210
|
+
blur: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface EffectColors {
|
|
214
|
+
colors: string[];
|
|
215
|
+
hueShift: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface EffectLayer {
|
|
219
|
+
inset: number;
|
|
220
|
+
opacity: number;
|
|
221
|
+
scale: number;
|
|
222
|
+
background: string;
|
|
223
|
+
blur: string;
|
|
224
|
+
animation?: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface Orb {
|
|
228
|
+
x: number;
|
|
229
|
+
y: number;
|
|
230
|
+
size: number;
|
|
231
|
+
color: string;
|
|
232
|
+
opacity: number;
|
|
233
|
+
scale: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface MeshGradient {
|
|
237
|
+
width: string;
|
|
238
|
+
height: string;
|
|
239
|
+
top?: string;
|
|
240
|
+
bottom?: string;
|
|
241
|
+
left?: string;
|
|
242
|
+
right?: string;
|
|
243
|
+
color: string;
|
|
244
|
+
opacity: number;
|
|
245
|
+
scale: number;
|
|
246
|
+
rotation: number;
|
|
247
|
+
blur: string;
|
|
248
|
+
isCenter?: boolean;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface SpotlightData {
|
|
252
|
+
rotation: number;
|
|
253
|
+
inset: number;
|
|
254
|
+
colors: Array<{ color: string; opacity: number }>;
|
|
255
|
+
pulseInset: number;
|
|
256
|
+
pulseOpacity: number;
|
|
257
|
+
pulseScale: number;
|
|
258
|
+
ringOpacity: number;
|
|
259
|
+
ringScale: number;
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## `types/index.ts`
|
|
266
|
+
|
|
267
|
+
Re-export all types.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// Audio types
|
|
271
|
+
export type {
|
|
272
|
+
AudioSource,
|
|
273
|
+
PlaybackStatus,
|
|
274
|
+
WaveformOptions,
|
|
275
|
+
EqualizerOptions,
|
|
276
|
+
SharedWebAudioContext,
|
|
277
|
+
AudioContextState,
|
|
278
|
+
} from './audio';
|
|
279
|
+
|
|
280
|
+
// Component props
|
|
281
|
+
export type {
|
|
282
|
+
AudioPlayerProps,
|
|
283
|
+
AudioEqualizerProps,
|
|
284
|
+
AudioReactiveCoverProps,
|
|
285
|
+
AudioViewerProps,
|
|
286
|
+
} from './components';
|
|
287
|
+
|
|
288
|
+
// Effect types
|
|
289
|
+
export type {
|
|
290
|
+
EffectVariant,
|
|
291
|
+
EffectIntensity,
|
|
292
|
+
EffectColorScheme,
|
|
293
|
+
AudioLevels,
|
|
294
|
+
EffectConfig,
|
|
295
|
+
EffectColors,
|
|
296
|
+
EffectLayer,
|
|
297
|
+
Orb,
|
|
298
|
+
MeshGradient,
|
|
299
|
+
SpotlightData,
|
|
300
|
+
} from './effects';
|
|
301
|
+
```
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Phase 3: Hooks Extraction
|
|
2
|
+
|
|
3
|
+
## Source: `context.tsx` (lines 25-229)
|
|
4
|
+
|
|
5
|
+
Extract two internal hooks from context.tsx.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## `hooks/useSharedWebAudio.ts`
|
|
10
|
+
|
|
11
|
+
Web Audio API context management. Prevents "InvalidStateError" from multiple MediaElementSourceNodes.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
'use client';
|
|
15
|
+
|
|
16
|
+
import { useRef, useEffect, useCallback } from 'react';
|
|
17
|
+
import type { SharedWebAudioContext } from '../types';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Manages a shared Web Audio context and source node.
|
|
21
|
+
* All analyzers share the same source to prevent InvalidStateError.
|
|
22
|
+
*/
|
|
23
|
+
export function useSharedWebAudio(audioElement: HTMLMediaElement | null): SharedWebAudioContext {
|
|
24
|
+
const audioContextRef = useRef<AudioContext | null>(null);
|
|
25
|
+
const sourceRef = useRef<MediaElementAudioSourceNode | null>(null);
|
|
26
|
+
const connectedElementRef = useRef<HTMLMediaElement | null>(null);
|
|
27
|
+
const analyserNodesRef = useRef<Set<AnalyserNode>>(new Set());
|
|
28
|
+
|
|
29
|
+
// Initialize Web Audio on first play
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!audioElement) return;
|
|
32
|
+
|
|
33
|
+
if (connectedElementRef.current === audioElement && audioContextRef.current) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const initAudio = () => {
|
|
38
|
+
try {
|
|
39
|
+
if (!audioContextRef.current) {
|
|
40
|
+
const AudioContextClass = window.AudioContext ||
|
|
41
|
+
(window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
|
|
42
|
+
audioContextRef.current = new AudioContextClass();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const audioContext = audioContextRef.current;
|
|
46
|
+
|
|
47
|
+
if (connectedElementRef.current !== audioElement) {
|
|
48
|
+
if (sourceRef.current) {
|
|
49
|
+
try { sourceRef.current.disconnect(); } catch { /* ignore */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
sourceRef.current = audioContext.createMediaElementSource(audioElement);
|
|
53
|
+
sourceRef.current.connect(audioContext.destination);
|
|
54
|
+
connectedElementRef.current = audioElement;
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.warn('[SharedWebAudio] Could not initialize:', error);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handlePlay = () => {
|
|
62
|
+
initAudio();
|
|
63
|
+
if (audioContextRef.current?.state === 'suspended') {
|
|
64
|
+
audioContextRef.current.resume();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
audioElement.addEventListener('play', handlePlay);
|
|
69
|
+
if (!audioElement.paused) {
|
|
70
|
+
handlePlay();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return () => {
|
|
74
|
+
audioElement.removeEventListener('play', handlePlay);
|
|
75
|
+
};
|
|
76
|
+
}, [audioElement]);
|
|
77
|
+
|
|
78
|
+
const createAnalyser = useCallback((options?: { fftSize?: number; smoothing?: number }): AnalyserNode | null => {
|
|
79
|
+
if (!audioContextRef.current || !sourceRef.current) return null;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const analyser = audioContextRef.current.createAnalyser();
|
|
83
|
+
analyser.fftSize = options?.fftSize ?? 256;
|
|
84
|
+
analyser.smoothingTimeConstant = options?.smoothing ?? 0.85;
|
|
85
|
+
|
|
86
|
+
sourceRef.current.connect(analyser);
|
|
87
|
+
analyser.connect(audioContextRef.current.destination);
|
|
88
|
+
|
|
89
|
+
analyserNodesRef.current.add(analyser);
|
|
90
|
+
return analyser;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn('[SharedWebAudio] Could not create analyser:', error);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const disconnectAnalyser = useCallback((analyser: AnalyserNode) => {
|
|
98
|
+
try {
|
|
99
|
+
analyser.disconnect();
|
|
100
|
+
analyserNodesRef.current.delete(analyser);
|
|
101
|
+
} catch { /* ignore */ }
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
audioContext: audioContextRef.current,
|
|
106
|
+
sourceNode: sourceRef.current,
|
|
107
|
+
createAnalyser,
|
|
108
|
+
disconnectAnalyser,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## `hooks/useAudioAnalysis.ts`
|
|
116
|
+
|
|
117
|
+
Real-time audio frequency analysis for reactive effects.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
'use client';
|
|
121
|
+
|
|
122
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
123
|
+
import type { SharedWebAudioContext, AudioLevels } from '../types';
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Analyzes audio frequencies for bass, mid, high, and overall levels.
|
|
127
|
+
* Uses shared Web Audio context to prevent duplicate source nodes.
|
|
128
|
+
*/
|
|
129
|
+
export function useAudioAnalysis(
|
|
130
|
+
sharedAudio: SharedWebAudioContext,
|
|
131
|
+
isPlaying: boolean
|
|
132
|
+
): AudioLevels {
|
|
133
|
+
const [levels, setLevels] = useState<AudioLevels>({ bass: 0, mid: 0, high: 0, overall: 0 });
|
|
134
|
+
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
135
|
+
const animationRef = useRef<number | null>(null);
|
|
136
|
+
const dataArrayRef = useRef<Uint8Array | null>(null);
|
|
137
|
+
|
|
138
|
+
const cleanup = useCallback(() => {
|
|
139
|
+
if (animationRef.current) {
|
|
140
|
+
cancelAnimationFrame(animationRef.current);
|
|
141
|
+
animationRef.current = null;
|
|
142
|
+
}
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
// Create analyser when shared audio is ready
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!sharedAudio.sourceNode || analyserRef.current) return;
|
|
148
|
+
|
|
149
|
+
const analyser = sharedAudio.createAnalyser({ fftSize: 256, smoothing: 0.85 });
|
|
150
|
+
if (analyser) {
|
|
151
|
+
analyserRef.current = analyser;
|
|
152
|
+
dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return () => {
|
|
156
|
+
if (analyserRef.current) {
|
|
157
|
+
sharedAudio.disconnectAnalyser(analyserRef.current);
|
|
158
|
+
analyserRef.current = null;
|
|
159
|
+
dataArrayRef.current = null;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}, [sharedAudio.sourceNode, sharedAudio.createAnalyser, sharedAudio.disconnectAnalyser]);
|
|
163
|
+
|
|
164
|
+
// Animation loop
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!isPlaying || !analyserRef.current || !dataArrayRef.current) {
|
|
167
|
+
cleanup();
|
|
168
|
+
// Smooth fade out
|
|
169
|
+
setLevels(prev => ({
|
|
170
|
+
bass: prev.bass * 0.95 < 0.01 ? 0 : prev.bass * 0.95,
|
|
171
|
+
mid: prev.mid * 0.95,
|
|
172
|
+
high: prev.high * 0.95,
|
|
173
|
+
overall: prev.overall * 0.95,
|
|
174
|
+
}));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const analyser = analyserRef.current;
|
|
179
|
+
const dataArray = dataArrayRef.current;
|
|
180
|
+
|
|
181
|
+
const animate = () => {
|
|
182
|
+
analyser.getByteFrequencyData(dataArray);
|
|
183
|
+
const binCount = dataArray.length;
|
|
184
|
+
|
|
185
|
+
// Bass (0-15%)
|
|
186
|
+
const bassEnd = Math.floor(binCount * 0.15);
|
|
187
|
+
let bassSum = 0;
|
|
188
|
+
for (let i = 0; i < bassEnd; i++) bassSum += dataArray[i];
|
|
189
|
+
const bass = bassSum / bassEnd / 255;
|
|
190
|
+
|
|
191
|
+
// Mids (15-50%)
|
|
192
|
+
const midStart = bassEnd;
|
|
193
|
+
const midEnd = Math.floor(binCount * 0.5);
|
|
194
|
+
let midSum = 0;
|
|
195
|
+
for (let i = midStart; i < midEnd; i++) midSum += dataArray[i];
|
|
196
|
+
const mid = midSum / (midEnd - midStart) / 255;
|
|
197
|
+
|
|
198
|
+
// Highs (50-100%)
|
|
199
|
+
const highStart = midEnd;
|
|
200
|
+
let highSum = 0;
|
|
201
|
+
for (let i = highStart; i < binCount; i++) highSum += dataArray[i];
|
|
202
|
+
const high = highSum / (binCount - highStart) / 255;
|
|
203
|
+
|
|
204
|
+
// Overall
|
|
205
|
+
let totalSum = 0;
|
|
206
|
+
for (let i = 0; i < binCount; i++) totalSum += dataArray[i];
|
|
207
|
+
const overall = totalSum / binCount / 255;
|
|
208
|
+
|
|
209
|
+
// Smooth with lerp
|
|
210
|
+
setLevels(prev => ({
|
|
211
|
+
bass: prev.bass * 0.7 + bass * 0.3,
|
|
212
|
+
mid: prev.mid * 0.7 + mid * 0.3,
|
|
213
|
+
high: prev.high * 0.7 + high * 0.3,
|
|
214
|
+
overall: prev.overall * 0.7 + overall * 0.3,
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
animationRef.current = requestAnimationFrame(animate);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
animationRef.current = requestAnimationFrame(animate);
|
|
221
|
+
return cleanup;
|
|
222
|
+
}, [isPlaying, cleanup]);
|
|
223
|
+
|
|
224
|
+
return levels;
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## `hooks/useAudioHotkeys.ts`
|
|
231
|
+
|
|
232
|
+
Move existing file, update imports.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// Same content, just update imports:
|
|
236
|
+
// - import type { ... } from '../types';
|
|
237
|
+
// - import { useAudioControls, useAudioState } from '../context';
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## `hooks/useVisualization.tsx`
|
|
243
|
+
|
|
244
|
+
Rename from `useAudioVisualization.tsx`, update imports.
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// Same content, just update imports
|
|
248
|
+
// Export both old and new names for backward compatibility
|
|
249
|
+
export { useVisualization as useAudioVisualization };
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## `hooks/index.ts`
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
// Internal hooks (used by provider)
|
|
258
|
+
export { useSharedWebAudio } from './useSharedWebAudio';
|
|
259
|
+
export { useAudioAnalysis } from './useAudioAnalysis';
|
|
260
|
+
|
|
261
|
+
// Public hooks
|
|
262
|
+
export { useAudioHotkeys, AUDIO_SHORTCUTS } from './useAudioHotkeys';
|
|
263
|
+
export type { AudioHotkeyOptions, ShortcutItem, ShortcutGroup } from './useAudioHotkeys';
|
|
264
|
+
|
|
265
|
+
export {
|
|
266
|
+
useVisualization,
|
|
267
|
+
useAudioVisualization, // backward compat alias
|
|
268
|
+
VisualizationProvider,
|
|
269
|
+
VARIANT_INFO,
|
|
270
|
+
INTENSITY_INFO,
|
|
271
|
+
COLOR_SCHEME_INFO,
|
|
272
|
+
} from './useVisualization';
|
|
273
|
+
export type {
|
|
274
|
+
VisualizationSettings,
|
|
275
|
+
VisualizationVariant,
|
|
276
|
+
VisualizationIntensity,
|
|
277
|
+
VisualizationColorScheme,
|
|
278
|
+
UseAudioVisualizationReturn,
|
|
279
|
+
VisualizationProviderProps,
|
|
280
|
+
} from './useVisualization';
|
|
281
|
+
```
|