@djangocfg/ui-nextjs 2.1.66 → 2.1.68
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 +474 -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,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
|
+
```
|
|
@@ -286,16 +286,40 @@ interface WaveformOptions {
|
|
|
286
286
|
|
|
287
287
|
```
|
|
288
288
|
AudioPlayer/
|
|
289
|
-
├── index.ts
|
|
290
|
-
├── types
|
|
291
|
-
├──
|
|
292
|
-
├──
|
|
293
|
-
├──
|
|
294
|
-
|
|
295
|
-
├──
|
|
296
|
-
├──
|
|
297
|
-
├── useAudioHotkeys.ts
|
|
298
|
-
├──
|
|
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
|
|
299
323
|
└── effects/
|
|
300
|
-
└── index.ts
|
|
324
|
+
└── index.ts # Effect calculations
|
|
301
325
|
```
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* AudioEqualizer - Real-time frequency visualizer with animated bars
|
|
5
5
|
*
|
|
6
|
-
* Uses Web Audio
|
|
7
|
-
*
|
|
6
|
+
* Uses shared Web Audio context from AudioProvider for real-time frequency analysis.
|
|
7
|
+
* This prevents "InvalidStateError" from creating multiple MediaElementSourceNodes.
|
|
8
8
|
*
|
|
9
9
|
* Features:
|
|
10
10
|
* - Real-time frequency visualization
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
* - Theme-aware colors (dark/light support)
|
|
13
13
|
* - Smooth animations with CSS transitions
|
|
14
14
|
* - Peak hold indicators
|
|
15
|
+
* - Pre-allocated buffers for performance
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
18
19
|
import { cn } from '@djangocfg/ui-nextjs';
|
|
19
|
-
import { useAudioElement } from '
|
|
20
|
-
import type { AudioEqualizerProps } from '
|
|
20
|
+
import { useAudioElement } from '../context';
|
|
21
|
+
import type { AudioEqualizerProps } from '../types';
|
|
21
22
|
|
|
22
23
|
// =============================================================================
|
|
23
24
|
// CONSTANTS
|
|
@@ -42,8 +43,8 @@ export function AudioEqualizer({
|
|
|
42
43
|
peakColor,
|
|
43
44
|
className,
|
|
44
45
|
}: AudioEqualizerProps) {
|
|
45
|
-
// Get audio
|
|
46
|
-
const {
|
|
46
|
+
// Get shared audio context from AudioProvider
|
|
47
|
+
const { sharedAudio, isPlaying } = useAudioElement();
|
|
47
48
|
|
|
48
49
|
const [frequencies, setFrequencies] = useState<number[]>(() =>
|
|
49
50
|
new Array(barCount).fill(0)
|
|
@@ -52,13 +53,12 @@ export function AudioEqualizer({
|
|
|
52
53
|
new Array(barCount).fill(0)
|
|
53
54
|
);
|
|
54
55
|
|
|
55
|
-
// Refs for
|
|
56
|
-
const audioContextRef = useRef<AudioContext | null>(null);
|
|
56
|
+
// Refs for analyzer and animation
|
|
57
57
|
const analyserRef = useRef<AnalyserNode | null>(null);
|
|
58
|
-
const sourceRef = useRef<MediaElementAudioSourceNode | null>(null);
|
|
59
58
|
const animationRef = useRef<number | null>(null);
|
|
60
59
|
const peakTimersRef = useRef<number[]>(new Array(barCount).fill(0));
|
|
61
|
-
|
|
60
|
+
// Pre-allocated buffer for frequency data (prevents allocation per frame)
|
|
61
|
+
const dataArrayRef = useRef<Uint8Array | null>(null);
|
|
62
62
|
|
|
63
63
|
// Cleanup function
|
|
64
64
|
const cleanup = useCallback(() => {
|
|
@@ -68,82 +68,47 @@ export function AudioEqualizer({
|
|
|
68
68
|
}
|
|
69
69
|
}, []);
|
|
70
70
|
|
|
71
|
-
//
|
|
71
|
+
// Create analyzer using shared context
|
|
72
72
|
useEffect(() => {
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
// Wait for shared audio to be ready
|
|
74
|
+
if (!sharedAudio.sourceNode) {
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
//
|
|
79
|
-
if (
|
|
78
|
+
// Only create analyzer once
|
|
79
|
+
if (analyserRef.current) {
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const audioContext = audioContextRef.current;
|
|
90
|
-
|
|
91
|
-
if (!analyserRef.current) {
|
|
92
|
-
analyserRef.current = audioContext.createAnalyser();
|
|
93
|
-
analyserRef.current.fftSize = 64;
|
|
94
|
-
analyserRef.current.smoothingTimeConstant = 0.8;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (connectedElementRef.current !== audioElement) {
|
|
98
|
-
if (sourceRef.current) {
|
|
99
|
-
try {
|
|
100
|
-
sourceRef.current.disconnect();
|
|
101
|
-
} catch {
|
|
102
|
-
// Ignore disconnect errors
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
sourceRef.current = audioContext.createMediaElementSource(audioElement);
|
|
107
|
-
sourceRef.current.connect(analyserRef.current);
|
|
108
|
-
analyserRef.current.connect(audioContext.destination);
|
|
109
|
-
connectedElementRef.current = audioElement;
|
|
110
|
-
}
|
|
111
|
-
} catch (error) {
|
|
112
|
-
console.warn('AudioEqualizer: Could not connect to audio element', error);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const handlePlay = () => {
|
|
117
|
-
initAudio();
|
|
118
|
-
if (audioContextRef.current?.state === 'suspended') {
|
|
119
|
-
audioContextRef.current.resume();
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
audioElement.addEventListener('play', handlePlay);
|
|
124
|
-
|
|
125
|
-
if (!audioElement.paused) {
|
|
126
|
-
handlePlay();
|
|
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);
|
|
127
88
|
}
|
|
128
89
|
|
|
129
90
|
return () => {
|
|
130
|
-
|
|
91
|
+
if (analyserRef.current) {
|
|
92
|
+
sharedAudio.disconnectAnalyser(analyserRef.current);
|
|
93
|
+
analyserRef.current = null;
|
|
94
|
+
dataArrayRef.current = null;
|
|
95
|
+
}
|
|
131
96
|
};
|
|
132
|
-
}, [
|
|
97
|
+
}, [sharedAudio.sourceNode, sharedAudio.createAnalyser, sharedAudio.disconnectAnalyser]);
|
|
133
98
|
|
|
134
99
|
// Animation loop
|
|
135
100
|
useEffect(() => {
|
|
136
|
-
if (!isPlaying || !analyserRef.current) {
|
|
101
|
+
if (!isPlaying || !analyserRef.current || !dataArrayRef.current) {
|
|
137
102
|
cleanup();
|
|
138
103
|
setFrequencies(new Array(barCount).fill(0));
|
|
139
104
|
return;
|
|
140
105
|
}
|
|
141
106
|
|
|
142
107
|
const analyser = analyserRef.current;
|
|
143
|
-
const dataArray =
|
|
108
|
+
const dataArray = dataArrayRef.current;
|
|
144
109
|
|
|
145
110
|
const animate = () => {
|
|
146
|
-
analyser.getByteFrequencyData(dataArray);
|
|
111
|
+
analyser.getByteFrequencyData(dataArray as Uint8Array<ArrayBuffer>);
|
|
147
112
|
|
|
148
113
|
const step = Math.floor(dataArray.length / barCount);
|
|
149
114
|
const newFrequencies: number[] = [];
|
|
@@ -18,24 +18,15 @@ import {
|
|
|
18
18
|
Volume2,
|
|
19
19
|
VolumeX,
|
|
20
20
|
Loader2,
|
|
21
|
+
Repeat,
|
|
21
22
|
} from 'lucide-react';
|
|
22
23
|
import { Button, Slider, cn } from '@djangocfg/ui-nextjs';
|
|
23
|
-
import { useAudio } from '
|
|
24
|
+
import { useAudio } from '../context';
|
|
24
25
|
import { AudioEqualizer } from './AudioEqualizer';
|
|
25
|
-
import { useAudioHotkeys } from '
|
|
26
|
+
import { useAudioHotkeys } from '../hooks';
|
|
26
27
|
import { AudioShortcutsPopover } from './AudioShortcutsPopover';
|
|
27
|
-
import
|
|
28
|
-
|
|
29
|
-
// =============================================================================
|
|
30
|
-
// HELPERS
|
|
31
|
-
// =============================================================================
|
|
32
|
-
|
|
33
|
-
function formatTime(seconds: number): string {
|
|
34
|
-
if (!seconds || !isFinite(seconds) || seconds < 0) return '0:00';
|
|
35
|
-
const mins = Math.floor(seconds / 60);
|
|
36
|
-
const secs = Math.floor(seconds % 60);
|
|
37
|
-
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
38
|
-
}
|
|
28
|
+
import { formatTime } from '../utils';
|
|
29
|
+
import type { AudioPlayerProps } from '../types';
|
|
39
30
|
|
|
40
31
|
// =============================================================================
|
|
41
32
|
// COMPONENT
|
|
@@ -49,6 +40,7 @@ export const AudioPlayer = forwardRef<HTMLDivElement, AudioPlayerProps>(
|
|
|
49
40
|
showEqualizer = false,
|
|
50
41
|
showTimer = true,
|
|
51
42
|
showVolume = true,
|
|
43
|
+
showLoop = true,
|
|
52
44
|
equalizerOptions = {},
|
|
53
45
|
className,
|
|
54
46
|
style,
|
|
@@ -63,11 +55,13 @@ export const AudioPlayer = forwardRef<HTMLDivElement, AudioPlayerProps>(
|
|
|
63
55
|
duration,
|
|
64
56
|
volume,
|
|
65
57
|
isMuted,
|
|
58
|
+
isLooping,
|
|
66
59
|
togglePlay,
|
|
67
60
|
restart,
|
|
68
61
|
skip,
|
|
69
62
|
setVolume,
|
|
70
63
|
toggleMute,
|
|
64
|
+
toggleLoop,
|
|
71
65
|
} = useAudio();
|
|
72
66
|
|
|
73
67
|
// Enable keyboard shortcuts
|
|
@@ -211,6 +205,20 @@ export const AudioPlayer = forwardRef<HTMLDivElement, AudioPlayerProps>(
|
|
|
211
205
|
</>
|
|
212
206
|
)}
|
|
213
207
|
|
|
208
|
+
{/* Loop/Repeat */}
|
|
209
|
+
{showLoop && (
|
|
210
|
+
<Button
|
|
211
|
+
variant="ghost"
|
|
212
|
+
size="icon"
|
|
213
|
+
className={cn('h-9 w-9', isLooping && 'text-primary')}
|
|
214
|
+
onClick={toggleLoop}
|
|
215
|
+
disabled={!isReady}
|
|
216
|
+
title={isLooping ? 'Disable loop' : 'Enable loop'}
|
|
217
|
+
>
|
|
218
|
+
<Repeat className="h-4 w-4" />
|
|
219
|
+
</Button>
|
|
220
|
+
)}
|
|
221
|
+
|
|
214
222
|
{/* Shortcuts help */}
|
|
215
223
|
<AudioShortcutsPopover compact />
|
|
216
224
|
</div>
|
package/src/tools/AudioPlayer/{AudioShortcutsPopover.tsx → components/AudioShortcutsPopover.tsx}
RENAMED
|
@@ -19,10 +19,10 @@ import {
|
|
|
19
19
|
TooltipContent,
|
|
20
20
|
} from '@djangocfg/ui-nextjs';
|
|
21
21
|
import { Keyboard } from 'lucide-react';
|
|
22
|
-
import { AUDIO_SHORTCUTS } from '
|
|
22
|
+
import { AUDIO_SHORTCUTS } from '../hooks';
|
|
23
23
|
|
|
24
24
|
// =============================================================================
|
|
25
|
-
//
|
|
25
|
+
// TYPES
|
|
26
26
|
// =============================================================================
|
|
27
27
|
|
|
28
28
|
interface AudioShortcutsPopoverProps {
|
|
@@ -30,6 +30,10 @@ interface AudioShortcutsPopoverProps {
|
|
|
30
30
|
compact?: boolean;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// =============================================================================
|
|
34
|
+
// COMPONENT
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
33
37
|
export function AudioShortcutsPopover({ compact = false }: AudioShortcutsPopoverProps) {
|
|
34
38
|
const trigger = (
|
|
35
39
|
<PopoverTrigger asChild>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AudioReactiveCover - Album art with audio-reactive animations
|
|
5
|
+
*
|
|
6
|
+
* Uses effects utilities for clean data preparation before render.
|
|
7
|
+
* Click on cover to switch between effect variants.
|
|
8
|
+
*
|
|
9
|
+
* Audio analysis is performed in the AudioProvider context to persist
|
|
10
|
+
* across variant changes (including switching to 'none' and back).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { type ReactNode } from 'react';
|
|
14
|
+
import { cn } from '@djangocfg/ui-nextjs';
|
|
15
|
+
import { useAudioElement } from '../../context';
|
|
16
|
+
import {
|
|
17
|
+
type EffectVariant,
|
|
18
|
+
type EffectIntensity,
|
|
19
|
+
type EffectColorScheme,
|
|
20
|
+
getEffectConfig,
|
|
21
|
+
prepareEffectColors,
|
|
22
|
+
calculateGlowLayers,
|
|
23
|
+
calculateOrbs,
|
|
24
|
+
calculateMeshGradients,
|
|
25
|
+
calculateSpotlight,
|
|
26
|
+
EFFECT_ANIMATIONS,
|
|
27
|
+
} from '../../effects';
|
|
28
|
+
import { GlowEffect, OrbsEffect, SpotlightEffect, MeshEffect, type GlowEffectData } from './effects';
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// TYPES
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
export interface AudioReactiveCoverProps {
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
size?: 'sm' | 'md' | 'lg';
|
|
37
|
+
variant?: EffectVariant;
|
|
38
|
+
intensity?: EffectIntensity;
|
|
39
|
+
colorScheme?: EffectColorScheme;
|
|
40
|
+
onClick?: () => void;
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// CONSTANTS
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
const SIZES = {
|
|
49
|
+
sm: { container: 'w-32 h-32', orbBase: 40 },
|
|
50
|
+
md: { container: 'w-40 h-40', orbBase: 50 },
|
|
51
|
+
lg: { container: 'w-48 h-48', orbBase: 60 },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// COMPONENT
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
export function AudioReactiveCover({
|
|
59
|
+
children,
|
|
60
|
+
size = 'lg',
|
|
61
|
+
variant = 'spotlight',
|
|
62
|
+
intensity = 'medium',
|
|
63
|
+
colorScheme = 'primary',
|
|
64
|
+
onClick,
|
|
65
|
+
className,
|
|
66
|
+
}: AudioReactiveCoverProps) {
|
|
67
|
+
// Get audio levels from context (persists across variant changes)
|
|
68
|
+
const { isPlaying, audioLevels: levels } = useAudioElement();
|
|
69
|
+
|
|
70
|
+
// =========================================================================
|
|
71
|
+
// PREPARE DATA BEFORE RENDER
|
|
72
|
+
// =========================================================================
|
|
73
|
+
|
|
74
|
+
const sizeConfig = SIZES[size];
|
|
75
|
+
const effectConfig = getEffectConfig(intensity);
|
|
76
|
+
const { colors, hueShift } = prepareEffectColors(colorScheme, levels);
|
|
77
|
+
|
|
78
|
+
// Calculate scale based on overall level
|
|
79
|
+
const containerScale = 1 + levels.overall * effectConfig.scale;
|
|
80
|
+
|
|
81
|
+
// Prepare effect-specific data - NO memoization for real-time reactivity
|
|
82
|
+
const glowData: GlowEffectData | null = variant === 'glow' ? {
|
|
83
|
+
layers: calculateGlowLayers(levels, effectConfig, colors),
|
|
84
|
+
hueShift,
|
|
85
|
+
showPulseRings: levels.bass > 0.5,
|
|
86
|
+
showSparkle: levels.high > 0.4,
|
|
87
|
+
} : null;
|
|
88
|
+
|
|
89
|
+
const orbsData = variant === 'orbs'
|
|
90
|
+
? calculateOrbs(levels, effectConfig, colors, sizeConfig.orbBase)
|
|
91
|
+
: null;
|
|
92
|
+
|
|
93
|
+
const meshData = variant === 'mesh'
|
|
94
|
+
? calculateMeshGradients(levels, effectConfig, colors)
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
const spotlightData = variant === 'spotlight'
|
|
98
|
+
? calculateSpotlight(levels, effectConfig, colors, levels.mid * 360)
|
|
99
|
+
: null;
|
|
100
|
+
|
|
101
|
+
// =========================================================================
|
|
102
|
+
// RENDER
|
|
103
|
+
// =========================================================================
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
className={cn('relative', sizeConfig.container, className)}
|
|
108
|
+
style={{
|
|
109
|
+
transform: `scale(${containerScale})`,
|
|
110
|
+
transition: 'transform 0.1s ease-out',
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{/* Effect layers */}
|
|
114
|
+
{glowData && (
|
|
115
|
+
<GlowEffect data={glowData} colors={colors} isPlaying={isPlaying} />
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{orbsData && (
|
|
119
|
+
<OrbsEffect orbs={orbsData} blur={effectConfig.blur} isPlaying={isPlaying} />
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{spotlightData && (
|
|
123
|
+
<SpotlightEffect data={spotlightData} colors={colors} blur={effectConfig.blur} isPlaying={isPlaying} />
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{meshData && (
|
|
127
|
+
<MeshEffect gradients={meshData} blur={effectConfig.blur} isPlaying={isPlaying} />
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Content (cover art) */}
|
|
131
|
+
<div
|
|
132
|
+
className="relative w-full h-full rounded-lg overflow-hidden shadow-2xl z-10 bg-background cursor-pointer"
|
|
133
|
+
onClick={onClick}
|
|
134
|
+
role={onClick ? 'button' : undefined}
|
|
135
|
+
tabIndex={onClick ? 0 : undefined}
|
|
136
|
+
onKeyDown={onClick ? (e) => e.key === 'Enter' && onClick() : undefined}
|
|
137
|
+
>
|
|
138
|
+
{children}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
{/* Inject animations once */}
|
|
142
|
+
<style dangerouslySetInnerHTML={{ __html: EFFECT_ANIMATIONS }} />
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default AudioReactiveCover;
|