@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.
Files changed (90) hide show
  1. package/package.json +8 -6
  2. package/src/stores/index.ts +8 -0
  3. package/src/stores/mediaCache.ts +464 -0
  4. package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
  5. package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
  6. package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
  7. package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
  8. package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
  9. package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
  10. package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
  11. package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
  12. package/src/tools/AudioPlayer/README.md +35 -11
  13. package/src/tools/AudioPlayer/{AudioEqualizer.tsx → components/AudioEqualizer.tsx} +29 -64
  14. package/src/tools/AudioPlayer/{AudioPlayer.tsx → components/AudioPlayer.tsx} +22 -14
  15. package/src/tools/AudioPlayer/{AudioShortcutsPopover.tsx → components/AudioShortcutsPopover.tsx} +6 -2
  16. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
  17. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
  18. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
  19. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
  20. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
  21. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
  22. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
  23. package/src/tools/AudioPlayer/{SimpleAudioPlayer.tsx → components/SimpleAudioPlayer.tsx} +12 -7
  24. package/src/tools/AudioPlayer/{VisualizationToggle.tsx → components/VisualizationToggle.tsx} +2 -6
  25. package/src/tools/AudioPlayer/components/index.ts +21 -0
  26. package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
  27. package/src/tools/AudioPlayer/context/index.ts +11 -0
  28. package/src/tools/AudioPlayer/context/selectors.ts +96 -0
  29. package/src/tools/AudioPlayer/hooks/index.ts +29 -0
  30. package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
  31. package/src/tools/AudioPlayer/{useAudioHotkeys.ts → hooks/useAudioHotkeys.ts} +11 -4
  32. package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
  33. package/src/tools/AudioPlayer/{useAudioVisualization.tsx → hooks/useVisualization.tsx} +11 -5
  34. package/src/tools/AudioPlayer/index.ts +104 -49
  35. package/src/tools/AudioPlayer/types/audio.ts +107 -0
  36. package/src/tools/AudioPlayer/{types.ts → types/components.ts} +20 -84
  37. package/src/tools/AudioPlayer/types/effects.ts +73 -0
  38. package/src/tools/AudioPlayer/types/index.ts +35 -0
  39. package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
  40. package/src/tools/AudioPlayer/utils/index.ts +5 -0
  41. package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
  42. package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
  43. package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
  44. package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
  45. package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
  46. package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
  47. package/src/tools/ImageViewer/README.md +16 -3
  48. package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
  49. package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
  50. package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
  51. package/src/tools/ImageViewer/components/index.ts +7 -0
  52. package/src/tools/ImageViewer/hooks/index.ts +9 -0
  53. package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
  54. package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
  55. package/src/tools/ImageViewer/index.ts +47 -3
  56. package/src/tools/ImageViewer/types.ts +75 -0
  57. package/src/tools/ImageViewer/utils/constants.ts +59 -0
  58. package/src/tools/ImageViewer/utils/index.ts +16 -0
  59. package/src/tools/ImageViewer/utils/lqip.ts +47 -0
  60. package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
  61. package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
  62. package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
  63. package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
  64. package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
  65. package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
  66. package/src/tools/VideoPlayer/README.md +26 -10
  67. package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
  68. package/src/tools/VideoPlayer/{VideoErrorFallback.tsx → components/VideoErrorFallback.tsx} +2 -2
  69. package/src/tools/VideoPlayer/{VideoPlayer.tsx → components/VideoPlayer.tsx} +4 -5
  70. package/src/tools/VideoPlayer/components/index.ts +14 -0
  71. package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
  72. package/src/tools/VideoPlayer/context/index.ts +8 -0
  73. package/src/tools/VideoPlayer/hooks/index.ts +9 -0
  74. package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
  75. package/src/tools/VideoPlayer/index.ts +29 -20
  76. package/src/tools/VideoPlayer/providers/StreamProvider.tsx +118 -28
  77. package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +89 -11
  78. package/src/tools/VideoPlayer/types/index.ts +38 -0
  79. package/src/tools/VideoPlayer/types/player.ts +116 -0
  80. package/src/tools/VideoPlayer/types/provider.ts +93 -0
  81. package/src/tools/VideoPlayer/types/sources.ts +97 -0
  82. package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
  83. package/src/tools/VideoPlayer/utils/index.ts +11 -0
  84. package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
  85. package/src/tools/index.ts +10 -0
  86. package/src/tools/AudioPlayer/AudioReactiveCover.tsx +0 -389
  87. package/src/tools/AudioPlayer/context.tsx +0 -426
  88. package/src/tools/ImageViewer/ImageViewer.tsx +0 -416
  89. package/src/tools/VideoPlayer/VideoPlayerContext.tsx +0 -125
  90. 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 # Exports
290
- ├── types.ts # Type definitions
291
- ├── context.tsx # AudioProvider, useAudio hooks
292
- ├── AudioPlayer.tsx # Main player component
293
- ├── AudioEqualizer.tsx # Frequency bars
294
- ├── AudioReactiveCover.tsx # Reactive album art
295
- ├── AudioShortcutsPopover.tsx # Shortcuts help
296
- ├── VisualizationToggle.tsx # Effect toggle button
297
- ├── useAudioHotkeys.ts # Keyboard shortcuts
298
- ├── useAudioVisualization.tsx # Visualization settings
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 # Effect calculations
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 API AnalyserNode for real-time frequency analysis
7
- * Renders animated vertical bars that respond to audio playback
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 './context';
20
- import type { AudioEqualizerProps } from './types';
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 element and playing state from context
46
- const { audioElement, isPlaying } = useAudioElement();
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 Web Audio API
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
- const connectedElementRef = useRef<HTMLMediaElement | null>(null);
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
- // Initialize Web Audio API
71
+ // Create analyzer using shared context
72
72
  useEffect(() => {
73
- if (!audioElement) {
74
- cleanup();
73
+ // Wait for shared audio to be ready
74
+ if (!sharedAudio.sourceNode) {
75
75
  return;
76
76
  }
77
77
 
78
- // Avoid reconnecting same element
79
- if (connectedElementRef.current === audioElement && audioContextRef.current) {
78
+ // Only create analyzer once
79
+ if (analyserRef.current) {
80
80
  return;
81
81
  }
82
82
 
83
- const initAudio = () => {
84
- try {
85
- if (!audioContextRef.current) {
86
- audioContextRef.current = new AudioContext();
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
- audioElement.removeEventListener('play', handlePlay);
91
+ if (analyserRef.current) {
92
+ sharedAudio.disconnectAnalyser(analyserRef.current);
93
+ analyserRef.current = null;
94
+ dataArrayRef.current = null;
95
+ }
131
96
  };
132
- }, [audioElement, cleanup]);
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 = new Uint8Array(analyser.frequencyBinCount);
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 './context';
24
+ import { useAudio } from '../context';
24
25
  import { AudioEqualizer } from './AudioEqualizer';
25
- import { useAudioHotkeys } from './useAudioHotkeys';
26
+ import { useAudioHotkeys } from '../hooks';
26
27
  import { AudioShortcutsPopover } from './AudioShortcutsPopover';
27
- import type { AudioPlayerProps } from './types';
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>
@@ -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 './useAudioHotkeys';
22
+ import { AUDIO_SHORTCUTS } from '../hooks';
23
23
 
24
24
  // =============================================================================
25
- // COMPONENT
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;