@djangocfg/ui-tools 2.1.91
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/dist/LottiePlayer.client-LBEC2JKY.mjs +161 -0
- package/dist/LottiePlayer.client-LBEC2JKY.mjs.map +1 -0
- package/dist/LottiePlayer.client-WFMG2OOW.cjs +168 -0
- package/dist/LottiePlayer.client-WFMG2OOW.cjs.map +1 -0
- package/dist/Mermaid.client-4TU2TSH3.mjs +477 -0
- package/dist/Mermaid.client-4TU2TSH3.mjs.map +1 -0
- package/dist/Mermaid.client-SBYY364Q.cjs +483 -0
- package/dist/Mermaid.client-SBYY364Q.cjs.map +1 -0
- package/dist/PlaygroundLayout-3YVSAEAF.cjs +1003 -0
- package/dist/PlaygroundLayout-3YVSAEAF.cjs.map +1 -0
- package/dist/PlaygroundLayout-4DYBORAS.mjs +996 -0
- package/dist/PlaygroundLayout-4DYBORAS.mjs.map +1 -0
- package/dist/PrettyCode.client-LCBPPTIX.mjs +152 -0
- package/dist/PrettyCode.client-LCBPPTIX.mjs.map +1 -0
- package/dist/PrettyCode.client-PNPLXRH6.cjs +154 -0
- package/dist/PrettyCode.client-PNPLXRH6.cjs.map +1 -0
- package/dist/chunk-37ZI6VD4.mjs +12 -0
- package/dist/chunk-37ZI6VD4.mjs.map +1 -0
- package/dist/chunk-3HK2OE62.cjs +81 -0
- package/dist/chunk-3HK2OE62.cjs.map +1 -0
- package/dist/chunk-7DGDQVQW.cjs +591 -0
- package/dist/chunk-7DGDQVQW.cjs.map +1 -0
- package/dist/chunk-M6P2FU7L.mjs +572 -0
- package/dist/chunk-M6P2FU7L.mjs.map +1 -0
- package/dist/chunk-UQ3XI5MY.cjs +15 -0
- package/dist/chunk-UQ3XI5MY.cjs.map +1 -0
- package/dist/chunk-YFRNE2IR.mjs +79 -0
- package/dist/chunk-YFRNE2IR.mjs.map +1 -0
- package/dist/index.cjs +5042 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1591 -0
- package/dist/index.d.ts +1591 -0
- package/dist/index.mjs +4941 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +86 -0
- package/src/components/markdown/MarkdownMessage.tsx +340 -0
- package/src/components/markdown/index.ts +5 -0
- package/src/index.ts +26 -0
- package/src/stores/index.ts +9 -0
- package/src/stores/mediaCache.ts +534 -0
- package/src/tools/AudioPlayer/README.md +206 -0
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +216 -0
- package/src/tools/AudioPlayer/components/HybridSimplePlayer.tsx +280 -0
- package/src/tools/AudioPlayer/components/HybridWaveform.tsx +279 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +149 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
- package/src/tools/AudioPlayer/components/index.ts +22 -0
- package/src/tools/AudioPlayer/context/HybridAudioProvider.tsx +158 -0
- package/src/tools/AudioPlayer/context/index.ts +16 -0
- package/src/tools/AudioPlayer/effects/index.ts +412 -0
- package/src/tools/AudioPlayer/hooks/index.ts +35 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudio.ts +387 -0
- package/src/tools/AudioPlayer/hooks/useHybridAudioAnalysis.ts +95 -0
- package/src/tools/AudioPlayer/hooks/useVisualization.tsx +207 -0
- package/src/tools/AudioPlayer/index.ts +133 -0
- package/src/tools/AudioPlayer/types/effects.ts +73 -0
- package/src/tools/AudioPlayer/types/index.ts +27 -0
- package/src/tools/AudioPlayer/utils/debug.ts +14 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
- package/src/tools/AudioPlayer/utils/index.ts +6 -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 +200 -0
- package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +145 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +241 -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 +204 -0
- package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
- package/src/tools/ImageViewer/index.ts +60 -0
- package/src/tools/ImageViewer/types.ts +81 -0
- package/src/tools/ImageViewer/utils/constants.ts +59 -0
- package/src/tools/ImageViewer/utils/debug.ts +14 -0
- package/src/tools/ImageViewer/utils/index.ts +17 -0
- package/src/tools/ImageViewer/utils/lqip.ts +47 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +197 -0
- package/src/tools/JsonForm/examples/BotConfigExample.tsx +249 -0
- package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +161 -0
- package/src/tools/JsonForm/index.ts +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +47 -0
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +74 -0
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +107 -0
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +35 -0
- package/src/tools/JsonForm/templates/FieldTemplate.tsx +62 -0
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +116 -0
- package/src/tools/JsonForm/templates/index.ts +12 -0
- package/src/tools/JsonForm/types.ts +83 -0
- package/src/tools/JsonForm/utils.ts +213 -0
- package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +37 -0
- package/src/tools/JsonForm/widgets/ColorWidget.tsx +219 -0
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +89 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +97 -0
- package/src/tools/JsonForm/widgets/SliderWidget.tsx +148 -0
- package/src/tools/JsonForm/widgets/SwitchWidget.tsx +35 -0
- package/src/tools/JsonForm/widgets/TextWidget.tsx +96 -0
- package/src/tools/JsonForm/widgets/index.ts +14 -0
- package/src/tools/JsonTree/index.tsx +243 -0
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +213 -0
- package/src/tools/LottiePlayer/index.tsx +56 -0
- package/src/tools/LottiePlayer/types.ts +108 -0
- package/src/tools/LottiePlayer/useLottie.ts +164 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +82 -0
- package/src/tools/Mermaid/components/MermaidCodeViewer.tsx +95 -0
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +103 -0
- package/src/tools/Mermaid/hooks/index.ts +4 -0
- package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +73 -0
- package/src/tools/Mermaid/hooks/useMermaidFullscreen.ts +46 -0
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +226 -0
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +29 -0
- package/src/tools/Mermaid/index.tsx +44 -0
- package/src/tools/Mermaid/utils/mermaid-helpers.ts +33 -0
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +149 -0
- package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +263 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +125 -0
- package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +100 -0
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +157 -0
- package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +173 -0
- package/src/tools/OpenapiViewer/components/VersionSelector.tsx +68 -0
- package/src/tools/OpenapiViewer/components/index.ts +14 -0
- package/src/tools/OpenapiViewer/constants.ts +39 -0
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +337 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
- package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +199 -0
- package/src/tools/OpenapiViewer/index.tsx +37 -0
- package/src/tools/OpenapiViewer/types.ts +151 -0
- package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
- package/src/tools/OpenapiViewer/utils/index.ts +9 -0
- package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +208 -0
- package/src/tools/PrettyCode/index.tsx +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 +264 -0
- package/src/tools/VideoPlayer/components/VideoControls.tsx +138 -0
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +172 -0
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +201 -0
- package/src/tools/VideoPlayer/components/index.ts +14 -0
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
- package/src/tools/VideoPlayer/context/index.ts +8 -0
- package/src/tools/VideoPlayer/hooks/index.ts +12 -0
- package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +70 -0
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +116 -0
- package/src/tools/VideoPlayer/index.ts +77 -0
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +284 -0
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +505 -0
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +400 -0
- package/src/tools/VideoPlayer/providers/index.ts +8 -0
- package/src/tools/VideoPlayer/types/index.ts +38 -0
- package/src/tools/VideoPlayer/types/player.ts +116 -0
- package/src/tools/VideoPlayer/types/provider.ts +93 -0
- package/src/tools/VideoPlayer/types/sources.ts +97 -0
- package/src/tools/VideoPlayer/utils/debug.ts +14 -0
- package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
- package/src/tools/VideoPlayer/utils/index.ts +12 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
- package/src/tools/_shared.ts +29 -0
- package/src/tools/index.ts +172 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# AudioPlayer
|
|
2
|
+
|
|
3
|
+
Audio player with native HTML5 streaming and audio-reactive visualizations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Native HTML5 audio playback (no crackling, native streaming support)
|
|
8
|
+
- Web Audio API for real-time frequency analysis
|
|
9
|
+
- Audio-reactive cover effects (glow, orbs, spotlight, mesh)
|
|
10
|
+
- Frequency visualization waveform
|
|
11
|
+
- Full playback controls
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { HybridSimplePlayer } from '@djangocfg/ui-nextjs';
|
|
17
|
+
|
|
18
|
+
// Simple usage
|
|
19
|
+
<HybridSimplePlayer src="https://example.com/audio.mp3" />
|
|
20
|
+
|
|
21
|
+
// With metadata and reactive cover
|
|
22
|
+
<HybridSimplePlayer
|
|
23
|
+
src={audioUrl}
|
|
24
|
+
title="Track Title"
|
|
25
|
+
artist="Artist Name"
|
|
26
|
+
coverArt="/path/to/cover.jpg"
|
|
27
|
+
reactiveCover
|
|
28
|
+
variant="spotlight"
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
// Full customization
|
|
32
|
+
<HybridSimplePlayer
|
|
33
|
+
src={audioUrl}
|
|
34
|
+
title="Track Title"
|
|
35
|
+
coverArt={coverUrl}
|
|
36
|
+
showWaveform
|
|
37
|
+
waveformMode="frequency" // 'frequency' | 'static'
|
|
38
|
+
showLoop
|
|
39
|
+
reactiveCover
|
|
40
|
+
variant="spotlight" // 'glow' | 'orbs' | 'spotlight' | 'mesh' | 'none'
|
|
41
|
+
intensity="medium" // 'subtle' | 'medium' | 'strong'
|
|
42
|
+
colorScheme="primary" // 'primary' | 'vibrant' | 'cool' | 'warm'
|
|
43
|
+
layout="horizontal" // 'vertical' | 'horizontal'
|
|
44
|
+
/>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Props
|
|
48
|
+
|
|
49
|
+
| Prop | Type | Default | Description |
|
|
50
|
+
|------|------|---------|-------------|
|
|
51
|
+
| `src` | `string` | required | Audio URL |
|
|
52
|
+
| `title` | `string` | - | Track title |
|
|
53
|
+
| `artist` | `string` | - | Artist name |
|
|
54
|
+
| `coverArt` | `string \| ReactNode` | - | Cover image URL or custom element |
|
|
55
|
+
| `coverSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Cover art size |
|
|
56
|
+
| `showWaveform` | `boolean` | `true` | Show frequency visualization |
|
|
57
|
+
| `waveformMode` | `'frequency' \| 'static'` | `'frequency'` | Visualization mode |
|
|
58
|
+
| `waveformHeight` | `number` | `64` | Waveform height in pixels |
|
|
59
|
+
| `showTimer` | `boolean` | `true` | Show time display |
|
|
60
|
+
| `showVolume` | `boolean` | `true` | Show volume control |
|
|
61
|
+
| `showLoop` | `boolean` | `true` | Show loop button |
|
|
62
|
+
| `reactiveCover` | `boolean` | `true` | Enable reactive effects |
|
|
63
|
+
| `variant` | `VisualizationVariant` | `'spotlight'` | Effect variant |
|
|
64
|
+
| `intensity` | `EffectIntensity` | `'medium'` | Effect intensity |
|
|
65
|
+
| `colorScheme` | `EffectColorScheme` | `'primary'` | Effect colors |
|
|
66
|
+
| `autoPlay` | `boolean` | `false` | Auto-play on load |
|
|
67
|
+
| `loop` | `boolean` | `false` | Loop playback |
|
|
68
|
+
| `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
|
|
69
|
+
|
|
70
|
+
## Advanced Usage
|
|
71
|
+
|
|
72
|
+
### HybridAudioProvider + HybridAudioPlayer
|
|
73
|
+
|
|
74
|
+
For custom layouts:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import {
|
|
78
|
+
HybridAudioProvider,
|
|
79
|
+
HybridAudioPlayer,
|
|
80
|
+
AudioReactiveCover,
|
|
81
|
+
useHybridAudioContext
|
|
82
|
+
} from '@djangocfg/ui-nextjs';
|
|
83
|
+
|
|
84
|
+
function MyPlayer({ audioUrl }: { audioUrl: string }) {
|
|
85
|
+
return (
|
|
86
|
+
<HybridAudioProvider src={audioUrl}>
|
|
87
|
+
<AudioReactiveCover variant="spotlight" onClick={handleClick}>
|
|
88
|
+
<img src={coverUrl} alt="Cover" />
|
|
89
|
+
</AudioReactiveCover>
|
|
90
|
+
<HybridAudioPlayer showWaveform showControls />
|
|
91
|
+
<CustomControls />
|
|
92
|
+
</HybridAudioProvider>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function CustomControls() {
|
|
97
|
+
const { state, controls, audioLevels } = useHybridAudioContext();
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div>
|
|
101
|
+
<p>Bass level: {(audioLevels.bass * 100).toFixed(0)}%</p>
|
|
102
|
+
<button onClick={controls.togglePlay}>
|
|
103
|
+
{state.isPlaying ? 'Pause' : 'Play'}
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Hooks
|
|
111
|
+
|
|
112
|
+
### useHybridAudioContext
|
|
113
|
+
|
|
114
|
+
Full context access:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
const {
|
|
118
|
+
state, // { isReady, isPlaying, currentTime, duration, volume, isMuted, isLooping }
|
|
119
|
+
controls, // { play, pause, togglePlay, seek, skip, setVolume, toggleMute, toggleLoop }
|
|
120
|
+
audioLevels, // { bass, mid, high, overall }
|
|
121
|
+
webAudio, // { context, analyser, sourceNode }
|
|
122
|
+
audioRef, // React ref to HTMLAudioElement
|
|
123
|
+
} = useHybridAudioContext();
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Specialized Hooks
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
// State only
|
|
130
|
+
const { isPlaying, currentTime, duration } = useHybridAudioState();
|
|
131
|
+
|
|
132
|
+
// Controls only (no re-render on time updates)
|
|
133
|
+
const { play, pause, togglePlay, seek } = useHybridAudioControls();
|
|
134
|
+
|
|
135
|
+
// Audio levels for reactive effects
|
|
136
|
+
const { bass, mid, high, overall } = useHybridAudioLevels();
|
|
137
|
+
|
|
138
|
+
// Web Audio API access
|
|
139
|
+
const { context, analyser, sourceNode } = useHybridWebAudio();
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Components
|
|
143
|
+
|
|
144
|
+
### AudioReactiveCover
|
|
145
|
+
|
|
146
|
+
Album art wrapper with audio-reactive effects:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
<AudioReactiveCover
|
|
150
|
+
variant="spotlight" // 'glow' | 'orbs' | 'spotlight' | 'mesh'
|
|
151
|
+
intensity="medium" // 'subtle' | 'medium' | 'strong'
|
|
152
|
+
colorScheme="primary" // 'primary' | 'vibrant' | 'cool' | 'warm'
|
|
153
|
+
onClick={() => nextVariant()}
|
|
154
|
+
>
|
|
155
|
+
<img src={coverArt} alt="Album cover" />
|
|
156
|
+
</AudioReactiveCover>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### HybridWaveform
|
|
160
|
+
|
|
161
|
+
Real-time frequency visualization:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
<HybridWaveform
|
|
165
|
+
mode="frequency" // 'frequency' | 'static'
|
|
166
|
+
height={64}
|
|
167
|
+
barWidth={3}
|
|
168
|
+
barGap={2}
|
|
169
|
+
/>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Effect Variants
|
|
173
|
+
|
|
174
|
+
| Variant | Description |
|
|
175
|
+
|---------|-------------|
|
|
176
|
+
| `spotlight` | Rotating conic gradient with bass pulse |
|
|
177
|
+
| `glow` | Multi-layered radial glows from edges |
|
|
178
|
+
| `orbs` | Floating orbs that react to frequencies |
|
|
179
|
+
| `mesh` | Large gradient blobs with movement |
|
|
180
|
+
| `none` | Effects disabled |
|
|
181
|
+
|
|
182
|
+
## Architecture
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
AudioPlayer/
|
|
186
|
+
├── index.ts # Public API exports
|
|
187
|
+
├── types/ # TypeScript types
|
|
188
|
+
├── hooks/
|
|
189
|
+
│ ├── useHybridAudio.ts # HTML5 audio + Web Audio hook
|
|
190
|
+
│ ├── useHybridAudioAnalysis.ts # Frequency analysis
|
|
191
|
+
│ └── useVisualization.tsx # Visualization settings
|
|
192
|
+
├── context/
|
|
193
|
+
│ └── HybridAudioProvider.tsx # Audio context provider
|
|
194
|
+
├── components/
|
|
195
|
+
│ ├── HybridAudioPlayer.tsx # Main player component
|
|
196
|
+
│ ├── HybridSimplePlayer.tsx # All-in-one wrapper
|
|
197
|
+
│ ├── HybridWaveform.tsx # Frequency visualization
|
|
198
|
+
│ └── ReactiveCover/ # Reactive effects
|
|
199
|
+
├── effects/ # Effect calculations
|
|
200
|
+
└── utils/ # Utilities
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Key design:
|
|
204
|
+
- HTML5 `<audio>` for playback (native streaming, no crackling)
|
|
205
|
+
- Web Audio API AnalyserNode for visualization only (not connected to output)
|
|
206
|
+
- Audio graph: `source → destination` + `source → analyser` (passive)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HybridAudioPlayer - Audio playback controls with frequency visualization
|
|
5
|
+
*
|
|
6
|
+
* Uses HybridAudioContext for state management.
|
|
7
|
+
* Native HTML5 audio for playback, Web Audio API for visualization only.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Real-time frequency visualization
|
|
11
|
+
* - Playback controls (play, pause, skip, restart)
|
|
12
|
+
* - Volume control with mute
|
|
13
|
+
* - Loop toggle
|
|
14
|
+
* - Keyboard shortcuts
|
|
15
|
+
* - Timer display
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { memo } from 'react';
|
|
19
|
+
import {
|
|
20
|
+
Play,
|
|
21
|
+
Pause,
|
|
22
|
+
RotateCcw,
|
|
23
|
+
SkipBack,
|
|
24
|
+
SkipForward,
|
|
25
|
+
Volume2,
|
|
26
|
+
VolumeX,
|
|
27
|
+
Loader2,
|
|
28
|
+
Repeat,
|
|
29
|
+
} from 'lucide-react';
|
|
30
|
+
import { Button, Slider, cn } from '../../_shared';
|
|
31
|
+
import { useHybridAudioContext } from '../context/HybridAudioProvider';
|
|
32
|
+
import { HybridWaveform } from './HybridWaveform';
|
|
33
|
+
import { formatTime } from '../utils';
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// TYPES
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
export interface HybridAudioPlayerProps {
|
|
40
|
+
/** Show playback controls */
|
|
41
|
+
showControls?: boolean;
|
|
42
|
+
/** Show frequency waveform */
|
|
43
|
+
showWaveform?: boolean;
|
|
44
|
+
/** Waveform visualization mode */
|
|
45
|
+
waveformMode?: 'frequency' | 'static';
|
|
46
|
+
/** Waveform height in pixels */
|
|
47
|
+
waveformHeight?: number;
|
|
48
|
+
/** Show time display */
|
|
49
|
+
showTimer?: boolean;
|
|
50
|
+
/** Show volume control */
|
|
51
|
+
showVolume?: boolean;
|
|
52
|
+
/** Show loop button */
|
|
53
|
+
showLoop?: boolean;
|
|
54
|
+
/** Additional CSS class */
|
|
55
|
+
className?: string;
|
|
56
|
+
/** Inline styles */
|
|
57
|
+
style?: React.CSSProperties;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// COMPONENT
|
|
62
|
+
// =============================================================================
|
|
63
|
+
|
|
64
|
+
export const HybridAudioPlayer = memo(function HybridAudioPlayer({
|
|
65
|
+
showControls = true,
|
|
66
|
+
showWaveform = true,
|
|
67
|
+
waveformMode = 'frequency',
|
|
68
|
+
waveformHeight = 64,
|
|
69
|
+
showTimer = true,
|
|
70
|
+
showVolume = true,
|
|
71
|
+
showLoop = true,
|
|
72
|
+
className,
|
|
73
|
+
style,
|
|
74
|
+
}: HybridAudioPlayerProps) {
|
|
75
|
+
const { state, controls } = useHybridAudioContext();
|
|
76
|
+
|
|
77
|
+
const isLoading = !state.isReady;
|
|
78
|
+
|
|
79
|
+
const handleVolumeChange = (value: number[]) => {
|
|
80
|
+
controls.setVolume(value[0] / 100);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
className={cn('flex flex-col gap-3 p-4 rounded-lg bg-card border', className)}
|
|
86
|
+
style={style}
|
|
87
|
+
>
|
|
88
|
+
{/* Frequency Waveform */}
|
|
89
|
+
{showWaveform && (
|
|
90
|
+
<div className="relative">
|
|
91
|
+
<HybridWaveform
|
|
92
|
+
mode={waveformMode}
|
|
93
|
+
height={waveformHeight}
|
|
94
|
+
className={cn(isLoading && 'opacity-50')}
|
|
95
|
+
/>
|
|
96
|
+
{isLoading && (
|
|
97
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
98
|
+
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{/* Timer */}
|
|
105
|
+
{showTimer && (
|
|
106
|
+
<div className="flex justify-between text-xs text-muted-foreground tabular-nums px-1">
|
|
107
|
+
<span>{formatTime(state.currentTime)}</span>
|
|
108
|
+
<span>{formatTime(state.duration)}</span>
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{/* Controls */}
|
|
113
|
+
{showControls && (
|
|
114
|
+
<div className="flex items-center justify-center gap-1">
|
|
115
|
+
{/* Restart */}
|
|
116
|
+
<Button
|
|
117
|
+
variant="ghost"
|
|
118
|
+
size="icon"
|
|
119
|
+
className="h-9 w-9"
|
|
120
|
+
onClick={controls.restart}
|
|
121
|
+
disabled={!state.isReady}
|
|
122
|
+
title="Restart"
|
|
123
|
+
>
|
|
124
|
+
<RotateCcw className="h-4 w-4" />
|
|
125
|
+
</Button>
|
|
126
|
+
|
|
127
|
+
{/* Skip back 5s */}
|
|
128
|
+
<Button
|
|
129
|
+
variant="ghost"
|
|
130
|
+
size="icon"
|
|
131
|
+
className="h-9 w-9"
|
|
132
|
+
onClick={() => controls.skip(-5)}
|
|
133
|
+
disabled={!state.isReady}
|
|
134
|
+
title="Back 5 seconds"
|
|
135
|
+
>
|
|
136
|
+
<SkipBack className="h-4 w-4" />
|
|
137
|
+
</Button>
|
|
138
|
+
|
|
139
|
+
{/* Play/Pause */}
|
|
140
|
+
<Button
|
|
141
|
+
variant="default"
|
|
142
|
+
size="icon"
|
|
143
|
+
className="h-12 w-12 rounded-full"
|
|
144
|
+
onClick={controls.togglePlay}
|
|
145
|
+
disabled={!state.isReady && !isLoading}
|
|
146
|
+
title={state.isPlaying ? 'Pause' : 'Play'}
|
|
147
|
+
>
|
|
148
|
+
{isLoading ? (
|
|
149
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
150
|
+
) : state.isPlaying ? (
|
|
151
|
+
<Pause className="h-5 w-5" />
|
|
152
|
+
) : (
|
|
153
|
+
<Play className="h-5 w-5 ml-0.5" />
|
|
154
|
+
)}
|
|
155
|
+
</Button>
|
|
156
|
+
|
|
157
|
+
{/* Skip forward 5s */}
|
|
158
|
+
<Button
|
|
159
|
+
variant="ghost"
|
|
160
|
+
size="icon"
|
|
161
|
+
className="h-9 w-9"
|
|
162
|
+
onClick={() => controls.skip(5)}
|
|
163
|
+
disabled={!state.isReady}
|
|
164
|
+
title="Forward 5 seconds"
|
|
165
|
+
>
|
|
166
|
+
<SkipForward className="h-4 w-4" />
|
|
167
|
+
</Button>
|
|
168
|
+
|
|
169
|
+
{/* Volume */}
|
|
170
|
+
{showVolume && (
|
|
171
|
+
<>
|
|
172
|
+
<Button
|
|
173
|
+
variant="ghost"
|
|
174
|
+
size="icon"
|
|
175
|
+
className="h-9 w-9"
|
|
176
|
+
onClick={controls.toggleMute}
|
|
177
|
+
title={state.isMuted ? 'Unmute' : 'Mute'}
|
|
178
|
+
>
|
|
179
|
+
{state.isMuted || state.volume === 0 ? (
|
|
180
|
+
<VolumeX className="h-4 w-4" />
|
|
181
|
+
) : (
|
|
182
|
+
<Volume2 className="h-4 w-4" />
|
|
183
|
+
)}
|
|
184
|
+
</Button>
|
|
185
|
+
|
|
186
|
+
<Slider
|
|
187
|
+
value={[state.isMuted ? 0 : state.volume * 100]}
|
|
188
|
+
max={100}
|
|
189
|
+
step={1}
|
|
190
|
+
onValueChange={handleVolumeChange}
|
|
191
|
+
className="w-20"
|
|
192
|
+
aria-label="Volume"
|
|
193
|
+
/>
|
|
194
|
+
</>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{/* Loop/Repeat */}
|
|
198
|
+
{showLoop && (
|
|
199
|
+
<Button
|
|
200
|
+
variant="ghost"
|
|
201
|
+
size="icon"
|
|
202
|
+
className={cn('h-9 w-9', state.isLooping && 'text-primary')}
|
|
203
|
+
onClick={controls.toggleLoop}
|
|
204
|
+
disabled={!state.isReady}
|
|
205
|
+
title={state.isLooping ? 'Disable loop' : 'Enable loop'}
|
|
206
|
+
>
|
|
207
|
+
<Repeat className="h-4 w-4" />
|
|
208
|
+
</Button>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
export default HybridAudioPlayer;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HybridSimplePlayer - Easy-to-use hybrid audio player wrapper
|
|
5
|
+
*
|
|
6
|
+
* Combines HybridAudioProvider + HybridAudioPlayer + optional reactive cover
|
|
7
|
+
* in a single component with sensible defaults.
|
|
8
|
+
*
|
|
9
|
+
* Uses native HTML5 audio for playback (no crackling) with Web Audio API
|
|
10
|
+
* for visualization only.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Minimal usage
|
|
14
|
+
* <HybridSimplePlayer src="https://example.com/audio.mp3" />
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // With cover art and reactive effects
|
|
18
|
+
* <HybridSimplePlayer
|
|
19
|
+
* src={audioUrl}
|
|
20
|
+
* title="Track Title"
|
|
21
|
+
* artist="Artist Name"
|
|
22
|
+
* coverArt="/path/to/cover.jpg"
|
|
23
|
+
* reactiveCover
|
|
24
|
+
* variant="spotlight"
|
|
25
|
+
* />
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { type ReactNode } from 'react';
|
|
29
|
+
import { Music } from 'lucide-react';
|
|
30
|
+
import { cn } from '@djangocfg/ui-core';
|
|
31
|
+
|
|
32
|
+
import { HybridAudioProvider } from '../context/HybridAudioProvider';
|
|
33
|
+
import { HybridAudioPlayer } from './HybridAudioPlayer';
|
|
34
|
+
import { AudioReactiveCover } from './ReactiveCover';
|
|
35
|
+
import { VisualizationProvider, useVisualization } from '../hooks';
|
|
36
|
+
import type { EffectIntensity, EffectColorScheme } from '../effects';
|
|
37
|
+
import type { VisualizationVariant } from '../hooks';
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// TYPES
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
export interface HybridSimplePlayerProps {
|
|
44
|
+
/** Audio source URL */
|
|
45
|
+
src: string;
|
|
46
|
+
|
|
47
|
+
/** Track title */
|
|
48
|
+
title?: string;
|
|
49
|
+
|
|
50
|
+
/** Artist name */
|
|
51
|
+
artist?: string;
|
|
52
|
+
|
|
53
|
+
/** Cover art URL or ReactNode */
|
|
54
|
+
coverArt?: string | ReactNode;
|
|
55
|
+
|
|
56
|
+
/** Cover art size */
|
|
57
|
+
coverSize?: 'sm' | 'md' | 'lg';
|
|
58
|
+
|
|
59
|
+
/** Show frequency waveform */
|
|
60
|
+
showWaveform?: boolean;
|
|
61
|
+
|
|
62
|
+
/** Waveform visualization mode */
|
|
63
|
+
waveformMode?: 'frequency' | 'static';
|
|
64
|
+
|
|
65
|
+
/** Waveform height in pixels */
|
|
66
|
+
waveformHeight?: number;
|
|
67
|
+
|
|
68
|
+
/** Show timer */
|
|
69
|
+
showTimer?: boolean;
|
|
70
|
+
|
|
71
|
+
/** Show volume control */
|
|
72
|
+
showVolume?: boolean;
|
|
73
|
+
|
|
74
|
+
/** Show loop/repeat button */
|
|
75
|
+
showLoop?: boolean;
|
|
76
|
+
|
|
77
|
+
/** Enable audio-reactive cover effects */
|
|
78
|
+
reactiveCover?: boolean;
|
|
79
|
+
|
|
80
|
+
/** Reactive effect variant */
|
|
81
|
+
variant?: VisualizationVariant;
|
|
82
|
+
|
|
83
|
+
/** Reactive effect intensity */
|
|
84
|
+
intensity?: EffectIntensity;
|
|
85
|
+
|
|
86
|
+
/** Reactive effect color scheme */
|
|
87
|
+
colorScheme?: EffectColorScheme;
|
|
88
|
+
|
|
89
|
+
/** Auto-play on load */
|
|
90
|
+
autoPlay?: boolean;
|
|
91
|
+
|
|
92
|
+
/** Loop playback */
|
|
93
|
+
loop?: boolean;
|
|
94
|
+
|
|
95
|
+
/** Initial volume (0-1) */
|
|
96
|
+
initialVolume?: number;
|
|
97
|
+
|
|
98
|
+
/** Layout direction */
|
|
99
|
+
layout?: 'vertical' | 'horizontal';
|
|
100
|
+
|
|
101
|
+
/** Additional class name */
|
|
102
|
+
className?: string;
|
|
103
|
+
|
|
104
|
+
/** Callbacks */
|
|
105
|
+
onPlay?: () => void;
|
|
106
|
+
onPause?: () => void;
|
|
107
|
+
onEnded?: () => void;
|
|
108
|
+
onError?: (error: Error) => void;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// CONSTANTS
|
|
113
|
+
// =============================================================================
|
|
114
|
+
|
|
115
|
+
const COVER_SIZES = {
|
|
116
|
+
sm: 'w-24 h-24',
|
|
117
|
+
md: 'w-32 h-32',
|
|
118
|
+
lg: 'w-48 h-48',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// =============================================================================
|
|
122
|
+
// COMPONENT
|
|
123
|
+
// =============================================================================
|
|
124
|
+
|
|
125
|
+
export function HybridSimplePlayer(props: HybridSimplePlayerProps) {
|
|
126
|
+
return (
|
|
127
|
+
<VisualizationProvider>
|
|
128
|
+
<HybridSimplePlayerContent {...props} />
|
|
129
|
+
</VisualizationProvider>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function HybridSimplePlayerContent({
|
|
134
|
+
src,
|
|
135
|
+
title,
|
|
136
|
+
artist,
|
|
137
|
+
coverArt,
|
|
138
|
+
coverSize = 'md',
|
|
139
|
+
showWaveform = true,
|
|
140
|
+
waveformMode = 'frequency',
|
|
141
|
+
waveformHeight = 64,
|
|
142
|
+
showTimer = true,
|
|
143
|
+
showVolume = true,
|
|
144
|
+
showLoop = true,
|
|
145
|
+
reactiveCover = true,
|
|
146
|
+
variant,
|
|
147
|
+
intensity,
|
|
148
|
+
colorScheme,
|
|
149
|
+
autoPlay = false,
|
|
150
|
+
loop = false,
|
|
151
|
+
initialVolume = 1,
|
|
152
|
+
layout = 'vertical',
|
|
153
|
+
className,
|
|
154
|
+
onPlay,
|
|
155
|
+
onPause,
|
|
156
|
+
onEnded,
|
|
157
|
+
onError,
|
|
158
|
+
}: HybridSimplePlayerProps) {
|
|
159
|
+
const { settings: vizSettings, nextVariant } = useVisualization();
|
|
160
|
+
|
|
161
|
+
// Determine effective variant (from props or localStorage settings)
|
|
162
|
+
const effectiveVariant =
|
|
163
|
+
variant ?? (vizSettings.variant !== 'none' ? vizSettings.variant : 'spotlight');
|
|
164
|
+
const effectiveIntensity = intensity ?? vizSettings.intensity;
|
|
165
|
+
const effectiveColorScheme = colorScheme ?? vizSettings.colorScheme;
|
|
166
|
+
|
|
167
|
+
// Show reactive cover if enabled and variant is not 'none'
|
|
168
|
+
const showReactiveCover = reactiveCover && effectiveVariant !== 'none';
|
|
169
|
+
|
|
170
|
+
// Render cover art content
|
|
171
|
+
const renderCoverContent = () => {
|
|
172
|
+
if (typeof coverArt === 'string') {
|
|
173
|
+
return (
|
|
174
|
+
<img src={coverArt} alt={title || 'Album cover'} className="w-full h-full object-cover" />
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (coverArt) {
|
|
179
|
+
return coverArt;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Default placeholder
|
|
183
|
+
return (
|
|
184
|
+
<div className="w-full h-full bg-muted/30 flex items-center justify-center">
|
|
185
|
+
<Music className="w-1/3 h-1/3 text-muted-foreground/50" />
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const isHorizontal = layout === 'horizontal';
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<HybridAudioProvider
|
|
194
|
+
src={src}
|
|
195
|
+
autoPlay={autoPlay}
|
|
196
|
+
loop={loop}
|
|
197
|
+
initialVolume={initialVolume}
|
|
198
|
+
onPlay={onPlay}
|
|
199
|
+
onPause={onPause}
|
|
200
|
+
onEnded={onEnded}
|
|
201
|
+
onError={onError}
|
|
202
|
+
>
|
|
203
|
+
<div
|
|
204
|
+
className={cn(
|
|
205
|
+
'flex gap-4',
|
|
206
|
+
isHorizontal ? 'flex-row items-center' : 'flex-col items-center',
|
|
207
|
+
className
|
|
208
|
+
)}
|
|
209
|
+
>
|
|
210
|
+
{/* Cover Art */}
|
|
211
|
+
{(coverArt || reactiveCover) && (
|
|
212
|
+
<div className="flex flex-col items-center gap-2 shrink-0">
|
|
213
|
+
{showReactiveCover ? (
|
|
214
|
+
<AudioReactiveCover
|
|
215
|
+
size={coverSize}
|
|
216
|
+
variant={effectiveVariant as 'glow' | 'orbs' | 'spotlight' | 'mesh'}
|
|
217
|
+
intensity={effectiveIntensity}
|
|
218
|
+
colorScheme={effectiveColorScheme}
|
|
219
|
+
onClick={nextVariant}
|
|
220
|
+
>
|
|
221
|
+
<div className={cn('rounded-lg overflow-hidden', COVER_SIZES[coverSize])}>
|
|
222
|
+
{renderCoverContent()}
|
|
223
|
+
</div>
|
|
224
|
+
</AudioReactiveCover>
|
|
225
|
+
) : (
|
|
226
|
+
<div
|
|
227
|
+
className={cn(
|
|
228
|
+
'rounded-lg overflow-hidden shadow-lg cursor-pointer',
|
|
229
|
+
COVER_SIZES[coverSize]
|
|
230
|
+
)}
|
|
231
|
+
onClick={nextVariant}
|
|
232
|
+
role="button"
|
|
233
|
+
tabIndex={0}
|
|
234
|
+
onKeyDown={(e) => e.key === 'Enter' && nextVariant()}
|
|
235
|
+
>
|
|
236
|
+
{renderCoverContent()}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
{/* Effect indicator */}
|
|
241
|
+
{reactiveCover && (
|
|
242
|
+
<span className="text-[10px] uppercase tracking-wider text-muted-foreground/50 select-none">
|
|
243
|
+
{vizSettings.variant === 'none' ? 'off' : vizSettings.variant}
|
|
244
|
+
</span>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{/* Track Info + Player */}
|
|
250
|
+
<div
|
|
251
|
+
className={cn('flex flex-col gap-3', isHorizontal ? 'flex-1 min-w-0' : 'w-full max-w-md')}
|
|
252
|
+
>
|
|
253
|
+
{/* Track Info */}
|
|
254
|
+
{(title || artist) && (
|
|
255
|
+
<div className={cn('text-center', isHorizontal && 'text-left')}>
|
|
256
|
+
{title && (
|
|
257
|
+
<h3 className="text-base font-medium text-foreground truncate">{title}</h3>
|
|
258
|
+
)}
|
|
259
|
+
{artist && <p className="text-sm text-muted-foreground truncate">{artist}</p>}
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{/* Audio Player */}
|
|
264
|
+
<HybridAudioPlayer
|
|
265
|
+
showControls
|
|
266
|
+
showWaveform={showWaveform}
|
|
267
|
+
waveformMode={waveformMode}
|
|
268
|
+
waveformHeight={waveformHeight}
|
|
269
|
+
showTimer={showTimer}
|
|
270
|
+
showVolume={showVolume}
|
|
271
|
+
showLoop={showLoop}
|
|
272
|
+
className="border-0 bg-transparent"
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</HybridAudioProvider>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export default HybridSimplePlayer;
|