@djangocfg/ui-tools 2.1.312 → 2.1.313

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 (161) hide show
  1. package/README.md +38 -22
  2. package/dist/{DocsLayout-W5JLRNSZ.mjs → DocsLayout-ESVQZO3V.mjs} +3 -3
  3. package/dist/{DocsLayout-W5JLRNSZ.mjs.map → DocsLayout-ESVQZO3V.mjs.map} +1 -1
  4. package/dist/{DocsLayout-ZXD2CUOH.cjs → DocsLayout-KUPDWJ3G.cjs} +48 -48
  5. package/dist/{DocsLayout-ZXD2CUOH.cjs.map → DocsLayout-KUPDWJ3G.cjs.map} +1 -1
  6. package/dist/Player-M3GC3VPE.mjs +4 -0
  7. package/dist/Player-M3GC3VPE.mjs.map +1 -0
  8. package/dist/Player-ZGQKKOWI.css +65 -0
  9. package/dist/Player-ZGQKKOWI.css.map +1 -0
  10. package/dist/Player-ZL2X5LGG.cjs +13 -0
  11. package/dist/Player-ZL2X5LGG.cjs.map +1 -0
  12. package/dist/{chunk-CXVGN6ZW.cjs → chunk-DFTVB66S.cjs} +7 -6
  13. package/dist/chunk-DFTVB66S.cjs.map +1 -0
  14. package/dist/{chunk-2QY3LJR6.mjs → chunk-EUADAUBQ.mjs} +5 -4
  15. package/dist/chunk-EUADAUBQ.mjs.map +1 -0
  16. package/dist/chunk-FX2QFYWF.mjs +2059 -0
  17. package/dist/chunk-FX2QFYWF.mjs.map +1 -0
  18. package/dist/{chunk-6HNAPVZ2.mjs → chunk-GBLQTHWT.mjs} +11 -13
  19. package/dist/chunk-GBLQTHWT.mjs.map +1 -0
  20. package/dist/{chunk-FYLR232K.cjs → chunk-S44PW6NK.cjs} +11 -13
  21. package/dist/chunk-S44PW6NK.cjs.map +1 -0
  22. package/dist/chunk-ZLQHUZDU.cjs +2061 -0
  23. package/dist/chunk-ZLQHUZDU.cjs.map +1 -0
  24. package/dist/components-WYEZL5TE.cjs +26 -0
  25. package/dist/{components-3RTH76CV.cjs.map → components-WYEZL5TE.cjs.map} +1 -1
  26. package/dist/components-ZAGG2PBO.mjs +5 -0
  27. package/dist/{components-5GVVL2Q6.mjs.map → components-ZAGG2PBO.mjs.map} +1 -1
  28. package/dist/index.cjs +36 -220
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.css +65 -0
  31. package/dist/index.css.map +1 -1
  32. package/dist/index.d.cts +44 -500
  33. package/dist/index.d.ts +44 -500
  34. package/dist/index.mjs +16 -62
  35. package/dist/index.mjs.map +1 -1
  36. package/package.json +6 -6
  37. package/src/components/markdown/MarkdownMessage/ActionRow.tsx +48 -0
  38. package/src/components/markdown/MarkdownMessage/ChatMessageRow.tsx +97 -0
  39. package/src/components/markdown/MarkdownMessage/CodeBlock.tsx +9 -13
  40. package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +77 -2
  41. package/src/components/markdown/MarkdownMessage/MarkdownMessage.tsx +2 -3
  42. package/src/components/markdown/MarkdownMessage/README.md +72 -0
  43. package/src/components/markdown/MarkdownMessage/components.tsx +3 -3
  44. package/src/components/markdown/MarkdownMessage/index.ts +6 -0
  45. package/src/index.ts +2 -11
  46. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +454 -107
  47. package/src/tools/AudioPlayer/Player.tsx +80 -0
  48. package/src/tools/AudioPlayer/PlayerShell.tsx +122 -0
  49. package/src/tools/AudioPlayer/README.md +139 -204
  50. package/src/tools/AudioPlayer/audio/audioContext.ts +39 -0
  51. package/src/tools/AudioPlayer/audio/decodePeaks.ts +36 -0
  52. package/src/tools/AudioPlayer/audio/index.ts +4 -0
  53. package/src/tools/AudioPlayer/audio/mediaElementSourceCache.ts +20 -0
  54. package/src/tools/AudioPlayer/audio/peaksCache.ts +37 -0
  55. package/src/tools/AudioPlayer/context/AudioRefContext.tsx +9 -0
  56. package/src/tools/AudioPlayer/context/ControlsContext.tsx +7 -0
  57. package/src/tools/AudioPlayer/context/LevelsContext.tsx +7 -0
  58. package/src/tools/AudioPlayer/context/MetaContext.tsx +16 -0
  59. package/src/tools/AudioPlayer/context/PlayerProvider.tsx +314 -0
  60. package/src/tools/AudioPlayer/context/StateContext.tsx +7 -0
  61. package/src/tools/AudioPlayer/context/index.ts +16 -15
  62. package/src/tools/AudioPlayer/context/selectors.ts +36 -0
  63. package/src/tools/AudioPlayer/hooks/index.ts +12 -39
  64. package/src/tools/AudioPlayer/hooks/useActivePlayer.ts +31 -0
  65. package/src/tools/AudioPlayer/hooks/useAnalyser.ts +62 -0
  66. package/src/tools/AudioPlayer/hooks/useAudioElementEvents.ts +102 -0
  67. package/src/tools/AudioPlayer/hooks/useKeyboardShortcuts.ts +91 -0
  68. package/src/tools/AudioPlayer/hooks/useMediaSession.ts +74 -0
  69. package/src/tools/AudioPlayer/hooks/usePeaks.ts +83 -0
  70. package/src/tools/AudioPlayer/hooks/usePlayerPreferences.ts +21 -0
  71. package/src/tools/AudioPlayer/hooks/usePlayheadLoop.ts +77 -0
  72. package/src/tools/AudioPlayer/hooks/useResizeObserver.ts +20 -0
  73. package/src/tools/AudioPlayer/hooks/useThemeWatcher.ts +22 -0
  74. package/src/tools/AudioPlayer/index.ts +63 -134
  75. package/src/tools/AudioPlayer/lazy.tsx +8 -97
  76. package/src/tools/AudioPlayer/parts/Controls/ControlsRow.tsx +30 -0
  77. package/src/tools/AudioPlayer/parts/Controls/IconButton.tsx +62 -0
  78. package/src/tools/AudioPlayer/parts/Controls/LoopButton.tsx +33 -0
  79. package/src/tools/AudioPlayer/parts/Controls/PlayButton.tsx +86 -0
  80. package/src/tools/AudioPlayer/parts/Controls/SkipButton.tsx +17 -0
  81. package/src/tools/AudioPlayer/parts/Controls/VolumeControl.tsx +171 -0
  82. package/src/tools/AudioPlayer/parts/Controls/index.ts +6 -0
  83. package/src/tools/AudioPlayer/parts/Cover/Cover.tsx +24 -0
  84. package/src/tools/AudioPlayer/parts/Cover/CoverPlaceholder.tsx +27 -0
  85. package/src/tools/AudioPlayer/parts/Cover/ReactivePulse.tsx +66 -0
  86. package/src/tools/AudioPlayer/parts/Cover/index.ts +3 -0
  87. package/src/tools/AudioPlayer/parts/ErrorState/ErrorState.tsx +35 -0
  88. package/src/tools/AudioPlayer/parts/ErrorState/index.ts +1 -0
  89. package/src/tools/AudioPlayer/parts/Layout/CompactLayout.tsx +25 -0
  90. package/src/tools/AudioPlayer/parts/Layout/DefaultLayout.tsx +48 -0
  91. package/src/tools/AudioPlayer/parts/Layout/index.ts +2 -0
  92. package/src/tools/AudioPlayer/parts/Meta/Artist.tsx +14 -0
  93. package/src/tools/AudioPlayer/parts/Meta/TimeDisplay.tsx +49 -0
  94. package/src/tools/AudioPlayer/parts/Meta/Title.tsx +13 -0
  95. package/src/tools/AudioPlayer/parts/Meta/index.ts +3 -0
  96. package/src/tools/AudioPlayer/parts/Skeleton/CoverSkeleton.tsx +13 -0
  97. package/src/tools/AudioPlayer/parts/Skeleton/MetaSkeleton.tsx +10 -0
  98. package/src/tools/AudioPlayer/parts/Skeleton/index.ts +2 -0
  99. package/src/tools/AudioPlayer/parts/Waveform/BarsWaveform.tsx +48 -0
  100. package/src/tools/AudioPlayer/parts/Waveform/LiveWaveform.tsx +95 -0
  101. package/src/tools/AudioPlayer/parts/Waveform/PeaksWaveform.tsx +100 -0
  102. package/src/tools/AudioPlayer/parts/Waveform/ProgressBar.tsx +76 -0
  103. package/src/tools/AudioPlayer/parts/Waveform/Waveform.tsx +74 -0
  104. package/src/tools/AudioPlayer/parts/Waveform/WaveformSkeleton.tsx +16 -0
  105. package/src/tools/AudioPlayer/parts/Waveform/index.ts +8 -0
  106. package/src/tools/AudioPlayer/parts/Waveform/waveformInteraction.ts +106 -0
  107. package/src/tools/AudioPlayer/parts/Waveform/waveformRenderer.ts +91 -0
  108. package/src/tools/AudioPlayer/parts/index.ts +1 -0
  109. package/src/tools/AudioPlayer/store/activePlayerBus.ts +63 -0
  110. package/src/tools/AudioPlayer/store/createLevelsStore.ts +37 -0
  111. package/src/tools/AudioPlayer/store/index.ts +16 -0
  112. package/src/tools/AudioPlayer/store/preferencesStore.ts +104 -0
  113. package/src/tools/AudioPlayer/styles/webview-safe.css +77 -0
  114. package/src/tools/AudioPlayer/types.ts +95 -0
  115. package/src/tools/AudioPlayer/utils/bucketize.ts +27 -0
  116. package/src/tools/AudioPlayer/utils/clamp.ts +5 -0
  117. package/src/tools/AudioPlayer/utils/dpr.ts +19 -0
  118. package/src/tools/AudioPlayer/utils/formatTime.ts +12 -8
  119. package/src/tools/AudioPlayer/utils/index.ts +4 -5
  120. package/src/tools/AudioPlayer/utils/readCssVar.ts +7 -0
  121. package/src/tools/AudioPlayer/utils/resolveCanvasColor.ts +28 -0
  122. package/src/tools/index.ts +5 -75
  123. package/dist/chunk-2QY3LJR6.mjs.map +0 -1
  124. package/dist/chunk-6HNAPVZ2.mjs.map +0 -1
  125. package/dist/chunk-CXVGN6ZW.cjs.map +0 -1
  126. package/dist/chunk-F2N7P5XU.cjs +0 -30
  127. package/dist/chunk-F2N7P5XU.cjs.map +0 -1
  128. package/dist/chunk-FYLR232K.cjs.map +0 -1
  129. package/dist/chunk-HMHIVEMS.mjs +0 -1619
  130. package/dist/chunk-HMHIVEMS.mjs.map +0 -1
  131. package/dist/chunk-JWB2EWQO.mjs +0 -5
  132. package/dist/chunk-JWB2EWQO.mjs.map +0 -1
  133. package/dist/chunk-YZX6FH3H.cjs +0 -1656
  134. package/dist/chunk-YZX6FH3H.cjs.map +0 -1
  135. package/dist/components-3RTH76CV.cjs +0 -27
  136. package/dist/components-5GVVL2Q6.mjs +0 -5
  137. package/dist/components-CPHOUQ5F.cjs +0 -46
  138. package/dist/components-CPHOUQ5F.cjs.map +0 -1
  139. package/dist/components-OTK43IMD.mjs +0 -6
  140. package/dist/components-OTK43IMD.mjs.map +0 -1
  141. package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +0 -225
  142. package/src/tools/AudioPlayer/components/HybridCompactPlayer.tsx +0 -163
  143. package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +0 -284
  144. package/src/tools/AudioPlayer/components/HybridWaveform.tsx +0 -286
  145. package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +0 -151
  146. package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +0 -110
  147. package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +0 -58
  148. package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +0 -45
  149. package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +0 -82
  150. package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +0 -8
  151. package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +0 -6
  152. package/src/tools/AudioPlayer/components/index.ts +0 -23
  153. package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +0 -158
  154. package/src/tools/AudioPlayer/effects/index.ts +0 -412
  155. package/src/tools/AudioPlayer/hooks/useAudioBus.ts +0 -76
  156. package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +0 -403
  157. package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +0 -96
  158. package/src/tools/AudioPlayer/hooks/useVisualization.tsx +0 -207
  159. package/src/tools/AudioPlayer/types/effects.ts +0 -73
  160. package/src/tools/AudioPlayer/types/index.ts +0 -27
  161. package/src/tools/AudioPlayer/utils/debug.ts +0 -14
@@ -1,76 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * useAudioBus — Global audio exclusivity via Zustand store.
5
- *
6
- * Ensures only one audio player plays at a time across the entire page.
7
- * No provider needed — singleton store, works anywhere in the tree.
8
- *
9
- * Also integrates with @djangocfg/ui-tools AudioPlayer automatically
10
- * (wired into useHybridAudio).
11
- *
12
- * @example
13
- * const { announce } = useAudioBus('my-player-id', () => pause());
14
- *
15
- * // When playback starts:
16
- * announce(); // all other players will pause
17
- */
18
-
19
- import { useEffect, useRef, useCallback } from 'react';
20
- import { create } from 'zustand';
21
-
22
- // =============================================================================
23
- // Store
24
- // =============================================================================
25
-
26
- interface AudioBusStore {
27
- /** ID of the currently active (playing) player, null if none */
28
- activeId: string | null;
29
- setActiveId: (id: string | null) => void;
30
- }
31
-
32
- export const useAudioBusStore = create<AudioBusStore>((set) => ({
33
- activeId: null,
34
- setActiveId: (id) => set({ activeId: id }),
35
- }));
36
-
37
- // =============================================================================
38
- // Hook
39
- // =============================================================================
40
-
41
- export interface UseAudioBusReturn {
42
- /** Broadcast: this player is now playing — all others should stop */
43
- announce: () => void;
44
- /** Release: this player stopped (clears activeId if it was ours) */
45
- release: () => void;
46
- }
47
-
48
- /**
49
- * @param playerId Stable unique ID for this player instance
50
- * @param onStop Called when another player announces — should pause this player
51
- */
52
- export function useAudioBus(playerId: string, onStop: () => void): UseAudioBusReturn {
53
- const onStopRef = useRef(onStop);
54
- onStopRef.current = onStop;
55
-
56
- const setActiveId = useAudioBusStore((s) => s.setActiveId);
57
-
58
- // Subscribe to store changes — if another player becomes active, stop this one
59
- useEffect(() => {
60
- return useAudioBusStore.subscribe((state) => {
61
- if (state.activeId !== null && state.activeId !== playerId) {
62
- onStopRef.current();
63
- }
64
- });
65
- }, [playerId]);
66
-
67
- const announce = useCallback(() => {
68
- setActiveId(playerId);
69
- }, [playerId, setActiveId]);
70
-
71
- const release = useCallback(() => {
72
- useAudioBusStore.setState((s) => s.activeId === playerId ? { activeId: null } : s);
73
- }, [playerId]);
74
-
75
- return { announce, release };
76
- }
@@ -1,403 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * useHybridAudio - Hybrid audio hook combining HTML5 audio + Web Audio API
5
- *
6
- * Uses native HTML5 <audio> for playback (no crackling, native streaming)
7
- * and Web Audio API only for visualization (AnalyserNode).
8
- *
9
- * Audio routing:
10
- * source -> destination (single path for playback)
11
- * source -> analyser (parallel path for visualization only, no output)
12
- */
13
-
14
- import { useCallback, useEffect, useId, useRef, useState } from 'react';
15
- import { useAudioBus } from './useAudioBus';
16
-
17
- // =============================================================================
18
- // TYPES
19
- // =============================================================================
20
-
21
- export interface UseHybridAudioOptions {
22
- src: string;
23
- autoPlay?: boolean;
24
- initialVolume?: number;
25
- loop?: boolean;
26
- crossOrigin?: 'anonymous' | 'use-credentials';
27
- /** Set to true to opt out of global audio bus (player won't stop others and won't be stopped) */
28
- excludeFromBus?: boolean;
29
- onPlay?: () => void;
30
- onPause?: () => void;
31
- onEnded?: () => void;
32
- onTimeUpdate?: (time: number) => void;
33
- onError?: (error: Error) => void;
34
- onReady?: () => void;
35
- }
36
-
37
- export interface HybridAudioState {
38
- isReady: boolean;
39
- isPlaying: boolean;
40
- currentTime: number;
41
- duration: number;
42
- volume: number;
43
- isMuted: boolean;
44
- isLooping: boolean;
45
- buffered: TimeRanges | null;
46
- error: Error | null;
47
- }
48
-
49
- export interface HybridAudioControls {
50
- play: () => Promise<void>;
51
- pause: () => void;
52
- togglePlay: () => void;
53
- seek: (time: number) => void;
54
- seekTo: (progress: number) => void;
55
- skip: (seconds: number) => void;
56
- setVolume: (vol: number) => void;
57
- toggleMute: () => void;
58
- toggleLoop: () => void;
59
- setLoop: (enabled: boolean) => void;
60
- restart: () => void;
61
- }
62
-
63
- export interface HybridWebAudioAPI {
64
- context: AudioContext | null;
65
- analyser: AnalyserNode | null;
66
- sourceNode: MediaElementAudioSourceNode | null;
67
- }
68
-
69
- export interface UseHybridAudioReturn {
70
- audioRef: React.RefObject<HTMLAudioElement | null>;
71
- state: HybridAudioState;
72
- controls: HybridAudioControls;
73
- webAudio: HybridWebAudioAPI;
74
- }
75
-
76
- // =============================================================================
77
- // HOOK
78
- // =============================================================================
79
-
80
- export function useHybridAudio(options: UseHybridAudioOptions): UseHybridAudioReturn {
81
- const {
82
- src,
83
- autoPlay = false,
84
- initialVolume = 1,
85
- loop = false,
86
- crossOrigin = 'anonymous',
87
- excludeFromBus = false,
88
- onPlay,
89
- onPause,
90
- onEnded,
91
- onTimeUpdate,
92
- onError,
93
- onReady,
94
- } = options;
95
-
96
- const playerId = useId();
97
-
98
- // Refs
99
- const audioRef = useRef<HTMLAudioElement | null>(null);
100
- const audioContextRef = useRef<AudioContext | null>(null);
101
- const sourceNodeRef = useRef<MediaElementAudioSourceNode | null>(null);
102
- const analyserRef = useRef<AnalyserNode | null>(null);
103
- const connectedElementRef = useRef<HTMLMediaElement | null>(null);
104
-
105
- // State
106
- const [state, setState] = useState<HybridAudioState>({
107
- isReady: false,
108
- isPlaying: false,
109
- currentTime: 0,
110
- duration: 0,
111
- volume: initialVolume,
112
- isMuted: false,
113
- isLooping: loop,
114
- buffered: null,
115
- error: null,
116
- });
117
-
118
- // Initialize Web Audio for visualization (lazy, on first play)
119
- const initWebAudio = useCallback(() => {
120
- const audio = audioRef.current;
121
- if (!audio) return;
122
-
123
- // Already connected to this element
124
- if (connectedElementRef.current === audio && audioContextRef.current) {
125
- return;
126
- }
127
-
128
- try {
129
- // Create AudioContext if needed
130
- if (!audioContextRef.current) {
131
- const AudioContextClass =
132
- window.AudioContext ||
133
- (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
134
- audioContextRef.current = new AudioContextClass();
135
- }
136
-
137
- const ctx = audioContextRef.current;
138
-
139
- // Disconnect old source if any
140
- if (sourceNodeRef.current) {
141
- try {
142
- sourceNodeRef.current.disconnect();
143
- } catch {
144
- /* ignore */
145
- }
146
- }
147
-
148
- // Create source from audio element
149
- const source = ctx.createMediaElementSource(audio);
150
- sourceNodeRef.current = source;
151
-
152
- // Create analyser for visualization
153
- const analyser = ctx.createAnalyser();
154
- analyser.fftSize = 256;
155
- analyser.smoothingTimeConstant = 0.85;
156
- analyserRef.current = analyser;
157
-
158
- // Audio routing:
159
- // 1. source -> destination (for playback)
160
- // 2. source -> analyser (for visualization only - NO output!)
161
- source.connect(ctx.destination);
162
- source.connect(analyser);
163
- // NOTE: analyser does NOT connect to destination - prevents double audio!
164
-
165
- connectedElementRef.current = audio;
166
- } catch (error) {
167
- console.warn('[useHybridAudio] Web Audio init failed:', error);
168
- }
169
- }, []);
170
-
171
- // Audio bus — stop other players when this one plays
172
- const { announce: busAnnounce, release: busRelease } = useAudioBus(
173
- playerId,
174
- useCallback(() => {
175
- if (!excludeFromBus) audioRef.current?.pause();
176
- }, [excludeFromBus])
177
- );
178
-
179
- // Resume AudioContext on user interaction
180
- const resumeAudioContext = useCallback(async () => {
181
- const ctx = audioContextRef.current;
182
- if (ctx && ctx.state === 'suspended') {
183
- await ctx.resume();
184
- }
185
- }, []);
186
-
187
- // Controls
188
- const play = useCallback(async () => {
189
- const audio = audioRef.current;
190
- if (!audio) return;
191
-
192
- try {
193
- if (!excludeFromBus) busAnnounce();
194
- initWebAudio();
195
- await resumeAudioContext();
196
- await audio.play();
197
- } catch (error) {
198
- console.error('[useHybridAudio] Play failed:', error);
199
- onError?.(error as Error);
200
- }
201
- }, [excludeFromBus, busAnnounce, initWebAudio, resumeAudioContext, onError]);
202
-
203
- const pause = useCallback(() => {
204
- audioRef.current?.pause();
205
- }, []);
206
-
207
- const togglePlay = useCallback(() => {
208
- if (state.isPlaying) {
209
- pause();
210
- } else {
211
- play();
212
- }
213
- }, [state.isPlaying, play, pause]);
214
-
215
- const seek = useCallback(
216
- (time: number) => {
217
- const audio = audioRef.current;
218
- if (audio && isFinite(time)) {
219
- audio.currentTime = Math.max(0, Math.min(time, state.duration || audio.duration || 0));
220
- }
221
- },
222
- [state.duration]
223
- );
224
-
225
- const seekTo = useCallback(
226
- (progress: number) => {
227
- const duration = state.duration || audioRef.current?.duration || 0;
228
- seek(duration * Math.max(0, Math.min(1, progress)));
229
- },
230
- [state.duration, seek]
231
- );
232
-
233
- const skip = useCallback(
234
- (seconds: number) => {
235
- seek(state.currentTime + seconds);
236
- },
237
- [state.currentTime, seek]
238
- );
239
-
240
- const setVolume = useCallback((vol: number) => {
241
- const audio = audioRef.current;
242
- if (audio) {
243
- const clampedVol = Math.max(0, Math.min(1, vol));
244
- audio.volume = clampedVol;
245
- setState((prev) => ({ ...prev, volume: clampedVol }));
246
- }
247
- }, []);
248
-
249
- const toggleMute = useCallback(() => {
250
- const audio = audioRef.current;
251
- if (audio) {
252
- audio.muted = !audio.muted;
253
- setState((prev) => ({ ...prev, isMuted: audio.muted }));
254
- }
255
- }, []);
256
-
257
- const toggleLoop = useCallback(() => {
258
- const audio = audioRef.current;
259
- if (audio) {
260
- audio.loop = !audio.loop;
261
- setState((prev) => ({ ...prev, isLooping: audio.loop }));
262
- }
263
- }, []);
264
-
265
- const setLoop = useCallback((enabled: boolean) => {
266
- const audio = audioRef.current;
267
- if (audio) {
268
- audio.loop = enabled;
269
- setState((prev) => ({ ...prev, isLooping: enabled }));
270
- }
271
- }, []);
272
-
273
- const restart = useCallback(() => {
274
- seek(0);
275
- play();
276
- }, [seek, play]);
277
-
278
- const controls: HybridAudioControls = {
279
- play,
280
- pause,
281
- togglePlay,
282
- seek,
283
- seekTo,
284
- skip,
285
- setVolume,
286
- toggleMute,
287
- toggleLoop,
288
- setLoop,
289
- restart,
290
- };
291
-
292
- // Create audio element on mount
293
- useEffect(() => {
294
- const audio = document.createElement('audio');
295
- audio.preload = 'metadata';
296
- audio.crossOrigin = crossOrigin;
297
- audio.volume = initialVolume;
298
- audio.loop = loop;
299
- audioRef.current = audio;
300
-
301
- return () => {
302
- audio.pause();
303
- audio.src = '';
304
- busRelease();
305
- if (audioContextRef.current) {
306
- audioContextRef.current.close().catch(() => {});
307
- }
308
- };
309
- // eslint-disable-next-line react-hooks/exhaustive-deps
310
- }, []);
311
-
312
- // Event handlers
313
- useEffect(() => {
314
- const audio = audioRef.current;
315
- if (!audio) return;
316
-
317
- const handlers = {
318
- loadedmetadata: () => {
319
- setState((prev) => ({
320
- ...prev,
321
- duration: audio.duration,
322
- isReady: true,
323
- }));
324
- onReady?.();
325
- },
326
- canplay: () => {
327
- setState((prev) => ({ ...prev, isReady: true }));
328
- if (autoPlay) {
329
- play();
330
- }
331
- },
332
- play: () => {
333
- setState((prev) => ({ ...prev, isPlaying: true }));
334
- onPlay?.();
335
- },
336
- pause: () => {
337
- setState((prev) => ({ ...prev, isPlaying: false }));
338
- onPause?.();
339
- },
340
- ended: () => {
341
- setState((prev) => ({ ...prev, isPlaying: false }));
342
- onEnded?.();
343
- },
344
- timeupdate: () => {
345
- setState((prev) => ({ ...prev, currentTime: audio.currentTime }));
346
- onTimeUpdate?.(audio.currentTime);
347
- },
348
- progress: () => {
349
- setState((prev) => ({ ...prev, buffered: audio.buffered }));
350
- },
351
- error: () => {
352
- const error = new Error(audio.error?.message || 'Audio error');
353
- setState((prev) => ({ ...prev, error }));
354
- onError?.(error);
355
- },
356
- volumechange: () => {
357
- setState((prev) => ({
358
- ...prev,
359
- volume: audio.volume,
360
- isMuted: audio.muted,
361
- }));
362
- },
363
- };
364
-
365
- Object.entries(handlers).forEach(([event, handler]) => {
366
- audio.addEventListener(event, handler);
367
- });
368
-
369
- return () => {
370
- Object.entries(handlers).forEach(([event, handler]) => {
371
- audio.removeEventListener(event, handler);
372
- });
373
- };
374
- }, [autoPlay, onPlay, onPause, onEnded, onTimeUpdate, onError, onReady, play]);
375
-
376
- // Load new source
377
- useEffect(() => {
378
- const audio = audioRef.current;
379
- if (!audio || !src) return;
380
-
381
- setState((prev) => ({
382
- ...prev,
383
- isReady: false,
384
- currentTime: 0,
385
- duration: 0,
386
- error: null,
387
- }));
388
-
389
- audio.src = src;
390
- audio.load();
391
- }, [src]);
392
-
393
- return {
394
- audioRef,
395
- state,
396
- controls,
397
- webAudio: {
398
- context: audioContextRef.current,
399
- analyser: analyserRef.current,
400
- sourceNode: sourceNodeRef.current,
401
- },
402
- };
403
- }
@@ -1,96 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * useHybridAudioAnalysis - Audio frequency analysis for hybrid player.
5
- *
6
- * Simplified version of useAudioAnalysis that works directly with AnalyserNode
7
- * instead of SharedWebAudioContext.
8
- */
9
-
10
- import { useCallback, useEffect, useRef, useState } from 'react';
11
-
12
- import type { AudioLevels } from '../effects';
13
-
14
- export function useHybridAudioAnalysis(
15
- analyser: AnalyserNode | null,
16
- isPlaying: boolean
17
- ): AudioLevels {
18
- const [levels, setLevels] = useState<AudioLevels>({ bass: 0, mid: 0, high: 0, overall: 0 });
19
- const animationRef = useRef<number | null>(null);
20
- const dataArrayRef = useRef<Uint8Array<ArrayBuffer> | null>(null);
21
-
22
- const cleanup = useCallback(() => {
23
- if (animationRef.current) {
24
- cancelAnimationFrame(animationRef.current);
25
- animationRef.current = null;
26
- }
27
- }, []);
28
-
29
- // Initialize data array when analyser is available
30
- useEffect(() => {
31
- if (analyser && !dataArrayRef.current) {
32
- dataArrayRef.current = new Uint8Array(analyser.frequencyBinCount) as Uint8Array<ArrayBuffer>;
33
- }
34
- }, [analyser]);
35
-
36
- // Animation loop
37
- useEffect(() => {
38
- if (!isPlaying || !analyser || !dataArrayRef.current) {
39
- cleanup();
40
- // Smooth fade out when stopped
41
- setLevels((prev) => ({
42
- bass: prev.bass * 0.95 < 0.01 ? 0 : prev.bass * 0.95,
43
- mid: prev.mid * 0.95 < 0.01 ? 0 : prev.mid * 0.95,
44
- high: prev.high * 0.95 < 0.01 ? 0 : prev.high * 0.95,
45
- overall: prev.overall * 0.95 < 0.01 ? 0 : prev.overall * 0.95,
46
- }));
47
- return;
48
- }
49
-
50
- const dataArray = dataArrayRef.current;
51
-
52
- const animate = () => {
53
- analyser.getByteFrequencyData(dataArray);
54
- const binCount = dataArray.length;
55
-
56
- // Bass (0-15% of frequency range)
57
- const bassEnd = Math.floor(binCount * 0.15);
58
- let bassSum = 0;
59
- for (let i = 0; i < bassEnd; i++) bassSum += dataArray[i];
60
- const bass = bassSum / bassEnd / 255;
61
-
62
- // Mids (15-50% of frequency range)
63
- const midStart = bassEnd;
64
- const midEnd = Math.floor(binCount * 0.5);
65
- let midSum = 0;
66
- for (let i = midStart; i < midEnd; i++) midSum += dataArray[i];
67
- const mid = midSum / (midEnd - midStart) / 255;
68
-
69
- // Highs (50-100% of frequency range)
70
- const highStart = midEnd;
71
- let highSum = 0;
72
- for (let i = highStart; i < binCount; i++) highSum += dataArray[i];
73
- const high = highSum / (binCount - highStart) / 255;
74
-
75
- // Overall
76
- let totalSum = 0;
77
- for (let i = 0; i < binCount; i++) totalSum += dataArray[i];
78
- const overall = totalSum / binCount / 255;
79
-
80
- // Smooth with lerp for natural feel
81
- setLevels((prev) => ({
82
- bass: prev.bass * 0.7 + bass * 0.3,
83
- mid: prev.mid * 0.7 + mid * 0.3,
84
- high: prev.high * 0.7 + high * 0.3,
85
- overall: prev.overall * 0.7 + overall * 0.3,
86
- }));
87
-
88
- animationRef.current = requestAnimationFrame(animate);
89
- };
90
-
91
- animationRef.current = requestAnimationFrame(animate);
92
- return cleanup;
93
- }, [analyser, isPlaying, cleanup]);
94
-
95
- return levels;
96
- }