@djangocfg/ui-nextjs 2.1.66 → 2.1.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -6
- package/src/stores/index.ts +8 -0
- package/src/stores/mediaCache.ts +464 -0
- package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
- package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
- package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
- package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
- package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
- package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
- package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
- package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
- package/src/tools/AudioPlayer/README.md +35 -11
- package/src/tools/AudioPlayer/{AudioEqualizer.tsx → components/AudioEqualizer.tsx} +29 -64
- package/src/tools/AudioPlayer/{AudioPlayer.tsx → components/AudioPlayer.tsx} +22 -14
- package/src/tools/AudioPlayer/{AudioShortcutsPopover.tsx → components/AudioShortcutsPopover.tsx} +6 -2
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
- package/src/tools/AudioPlayer/{SimpleAudioPlayer.tsx → components/SimpleAudioPlayer.tsx} +12 -7
- package/src/tools/AudioPlayer/{VisualizationToggle.tsx → components/VisualizationToggle.tsx} +2 -6
- package/src/tools/AudioPlayer/components/index.ts +21 -0
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
- package/src/tools/AudioPlayer/context/index.ts +11 -0
- package/src/tools/AudioPlayer/context/selectors.ts +96 -0
- package/src/tools/AudioPlayer/hooks/index.ts +29 -0
- package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
- package/src/tools/AudioPlayer/{useAudioHotkeys.ts → hooks/useAudioHotkeys.ts} +11 -4
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
- package/src/tools/AudioPlayer/{useAudioVisualization.tsx → hooks/useVisualization.tsx} +11 -5
- package/src/tools/AudioPlayer/index.ts +104 -49
- package/src/tools/AudioPlayer/types/audio.ts +107 -0
- package/src/tools/AudioPlayer/{types.ts → types/components.ts} +20 -84
- package/src/tools/AudioPlayer/types/effects.ts +73 -0
- package/src/tools/AudioPlayer/types/index.ts +35 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
- package/src/tools/AudioPlayer/utils/index.ts +5 -0
- package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
- package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
- package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
- package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
- package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
- package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
- package/src/tools/ImageViewer/README.md +16 -3
- package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
- package/src/tools/ImageViewer/components/index.ts +7 -0
- package/src/tools/ImageViewer/hooks/index.ts +9 -0
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
- package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
- package/src/tools/ImageViewer/index.ts +47 -3
- package/src/tools/ImageViewer/types.ts +75 -0
- package/src/tools/ImageViewer/utils/constants.ts +59 -0
- package/src/tools/ImageViewer/utils/index.ts +16 -0
- package/src/tools/ImageViewer/utils/lqip.ts +47 -0
- package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
- package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
- package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
- package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
- package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
- package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
- package/src/tools/VideoPlayer/README.md +26 -10
- package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
- package/src/tools/VideoPlayer/{VideoErrorFallback.tsx → components/VideoErrorFallback.tsx} +2 -2
- package/src/tools/VideoPlayer/{VideoPlayer.tsx → components/VideoPlayer.tsx} +4 -5
- package/src/tools/VideoPlayer/components/index.ts +14 -0
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
- package/src/tools/VideoPlayer/context/index.ts +8 -0
- package/src/tools/VideoPlayer/hooks/index.ts +9 -0
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
- package/src/tools/VideoPlayer/index.ts +29 -20
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +118 -28
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +89 -11
- package/src/tools/VideoPlayer/types/index.ts +38 -0
- package/src/tools/VideoPlayer/types/player.ts +116 -0
- package/src/tools/VideoPlayer/types/provider.ts +93 -0
- package/src/tools/VideoPlayer/types/sources.ts +97 -0
- package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
- package/src/tools/VideoPlayer/utils/index.ts +11 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
- package/src/tools/index.ts +10 -0
- package/src/tools/AudioPlayer/AudioReactiveCover.tsx +0 -389
- package/src/tools/AudioPlayer/context.tsx +0 -426
- package/src/tools/ImageViewer/ImageViewer.tsx +0 -416
- package/src/tools/VideoPlayer/VideoPlayerContext.tsx +0 -125
- package/src/tools/VideoPlayer/types.ts +0 -367
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-nextjs",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.67",
|
|
4
4
|
"description": "Next.js UI component library with Radix UI primitives, Tailwind CSS styling, charts, and form components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-components",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"check": "tsc --noEmit"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@djangocfg/api": "^2.1.
|
|
62
|
-
"@djangocfg/ui-core": "^2.1.
|
|
61
|
+
"@djangocfg/api": "^2.1.67",
|
|
62
|
+
"@djangocfg/ui-core": "^2.1.67",
|
|
63
63
|
"@types/react": "^19.1.0",
|
|
64
64
|
"@types/react-dom": "^19.1.0",
|
|
65
65
|
"consola": "^3.4.2",
|
|
@@ -70,7 +70,8 @@
|
|
|
70
70
|
"react-dom": "^19.1.0",
|
|
71
71
|
"react-hook-form": "7.65.0",
|
|
72
72
|
"tailwindcss": "^4.1.14",
|
|
73
|
-
"zod": "^4.1.13"
|
|
73
|
+
"zod": "^4.1.13",
|
|
74
|
+
"zustand": "^5.0.9"
|
|
74
75
|
},
|
|
75
76
|
"dependencies": {
|
|
76
77
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
@@ -109,11 +110,12 @@
|
|
|
109
110
|
"wavesurfer.js": "^7.12.1"
|
|
110
111
|
},
|
|
111
112
|
"devDependencies": {
|
|
112
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
113
|
+
"@djangocfg/typescript-config": "^2.1.67",
|
|
113
114
|
"@types/node": "^24.7.2",
|
|
114
115
|
"eslint": "^9.37.0",
|
|
115
116
|
"tailwindcss-animate": "1.0.7",
|
|
116
|
-
"typescript": "^5.9.3"
|
|
117
|
+
"typescript": "^5.9.3",
|
|
118
|
+
"zustand": "^5.0.9"
|
|
117
119
|
},
|
|
118
120
|
"publishConfig": {
|
|
119
121
|
"access": "public"
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { create } from 'zustand';
|
|
4
|
+
import { persist, devtools } from 'zustand/middleware';
|
|
5
|
+
|
|
6
|
+
// Types
|
|
7
|
+
interface BlobUrlEntry {
|
|
8
|
+
url: string;
|
|
9
|
+
refCount: number;
|
|
10
|
+
createdAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ImageDimensions {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface VideoMetadata {
|
|
19
|
+
duration: number;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
codec?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface EffectConfig {
|
|
26
|
+
opacity: number;
|
|
27
|
+
scale: number;
|
|
28
|
+
blur: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Stream URL TTL (5 minutes)
|
|
32
|
+
const STREAM_URL_TTL = 5 * 60 * 1000;
|
|
33
|
+
|
|
34
|
+
interface MediaCacheState {
|
|
35
|
+
// Blob URL cache (shared by Image, Audio, Video)
|
|
36
|
+
blobUrls: Map<string, BlobUrlEntry>;
|
|
37
|
+
|
|
38
|
+
// Image-specific
|
|
39
|
+
imageDimensions: Map<string, ImageDimensions>;
|
|
40
|
+
|
|
41
|
+
// Audio-specific
|
|
42
|
+
audioPlaybackPositions: Map<string, number>;
|
|
43
|
+
audioEffectConfigs: Map<string, EffectConfig>;
|
|
44
|
+
|
|
45
|
+
// Video-specific
|
|
46
|
+
videoStreamUrls: Map<string, { url: string; timestamp: number }>;
|
|
47
|
+
videoPosterUrls: Map<string, string>;
|
|
48
|
+
videoPlaybackPositions: Map<string, number>;
|
|
49
|
+
videoMetadata: Map<string, VideoMetadata>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface MediaCacheActions {
|
|
53
|
+
// Blob URL management (shared)
|
|
54
|
+
getOrCreateBlobUrl: (
|
|
55
|
+
key: string,
|
|
56
|
+
content: ArrayBuffer,
|
|
57
|
+
mimeType: string
|
|
58
|
+
) => string;
|
|
59
|
+
releaseBlobUrl: (key: string) => void;
|
|
60
|
+
hasBlobUrl: (key: string) => boolean;
|
|
61
|
+
|
|
62
|
+
// Image actions
|
|
63
|
+
cacheDimensions: (src: string, dims: ImageDimensions) => void;
|
|
64
|
+
getDimensions: (src: string) => ImageDimensions | null;
|
|
65
|
+
|
|
66
|
+
// Audio actions
|
|
67
|
+
saveAudioPosition: (uri: string, time: number) => void;
|
|
68
|
+
getAudioPosition: (uri: string) => number | null;
|
|
69
|
+
getEffectConfig: (key: string) => EffectConfig | null;
|
|
70
|
+
cacheEffectConfig: (key: string, config: EffectConfig) => void;
|
|
71
|
+
|
|
72
|
+
// Video actions
|
|
73
|
+
getOrCreateStreamUrl: (
|
|
74
|
+
sessionId: string,
|
|
75
|
+
path: string,
|
|
76
|
+
generator: (sessionId: string, path: string) => string
|
|
77
|
+
) => string;
|
|
78
|
+
getPosterUrl: (title: string) => string | null;
|
|
79
|
+
cachePosterUrl: (title: string, url: string) => void;
|
|
80
|
+
saveVideoPosition: (key: string, time: number) => void;
|
|
81
|
+
getVideoPosition: (key: string) => number | null;
|
|
82
|
+
cacheVideoMetadata: (url: string, meta: VideoMetadata) => void;
|
|
83
|
+
getVideoMetadata: (url: string) => VideoMetadata | null;
|
|
84
|
+
invalidateSession: (sessionId: string) => void;
|
|
85
|
+
|
|
86
|
+
// Global
|
|
87
|
+
clearCache: () => void;
|
|
88
|
+
getCacheStats: () => {
|
|
89
|
+
blobUrls: number;
|
|
90
|
+
dimensions: number;
|
|
91
|
+
audioPositions: number;
|
|
92
|
+
videoPositions: number;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type MediaCacheStore = MediaCacheState & MediaCacheActions;
|
|
97
|
+
|
|
98
|
+
// Initial state
|
|
99
|
+
const initialState: MediaCacheState = {
|
|
100
|
+
blobUrls: new Map(),
|
|
101
|
+
imageDimensions: new Map(),
|
|
102
|
+
audioPlaybackPositions: new Map(),
|
|
103
|
+
audioEffectConfigs: new Map(),
|
|
104
|
+
videoStreamUrls: new Map(),
|
|
105
|
+
videoPosterUrls: new Map(),
|
|
106
|
+
videoPlaybackPositions: new Map(),
|
|
107
|
+
videoMetadata: new Map(),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const useMediaCacheStore = create<MediaCacheStore>()(
|
|
111
|
+
devtools(
|
|
112
|
+
persist(
|
|
113
|
+
(set, get) => ({
|
|
114
|
+
...initialState,
|
|
115
|
+
|
|
116
|
+
// ========== Blob URL Management ==========
|
|
117
|
+
|
|
118
|
+
getOrCreateBlobUrl: (key, content, mimeType) => {
|
|
119
|
+
const existing = get().blobUrls.get(key);
|
|
120
|
+
if (existing) {
|
|
121
|
+
// Increment ref count
|
|
122
|
+
set(
|
|
123
|
+
(state) => ({
|
|
124
|
+
blobUrls: new Map(state.blobUrls).set(key, {
|
|
125
|
+
...existing,
|
|
126
|
+
refCount: existing.refCount + 1,
|
|
127
|
+
}),
|
|
128
|
+
}),
|
|
129
|
+
false,
|
|
130
|
+
'blobUrl/incrementRef'
|
|
131
|
+
);
|
|
132
|
+
return existing.url;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Create new blob URL
|
|
136
|
+
const blob = new Blob([content], { type: mimeType });
|
|
137
|
+
const url = URL.createObjectURL(blob);
|
|
138
|
+
set(
|
|
139
|
+
(state) => ({
|
|
140
|
+
blobUrls: new Map(state.blobUrls).set(key, {
|
|
141
|
+
url,
|
|
142
|
+
refCount: 1,
|
|
143
|
+
createdAt: Date.now(),
|
|
144
|
+
}),
|
|
145
|
+
}),
|
|
146
|
+
false,
|
|
147
|
+
'blobUrl/create'
|
|
148
|
+
);
|
|
149
|
+
return url;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
releaseBlobUrl: (key) => {
|
|
153
|
+
const entry = get().blobUrls.get(key);
|
|
154
|
+
if (!entry) return;
|
|
155
|
+
|
|
156
|
+
if (entry.refCount <= 1) {
|
|
157
|
+
// Last reference - revoke and remove
|
|
158
|
+
URL.revokeObjectURL(entry.url);
|
|
159
|
+
set(
|
|
160
|
+
(state) => {
|
|
161
|
+
const newMap = new Map(state.blobUrls);
|
|
162
|
+
newMap.delete(key);
|
|
163
|
+
return { blobUrls: newMap };
|
|
164
|
+
},
|
|
165
|
+
false,
|
|
166
|
+
'blobUrl/revoke'
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
// Decrement ref count
|
|
170
|
+
set(
|
|
171
|
+
(state) => ({
|
|
172
|
+
blobUrls: new Map(state.blobUrls).set(key, {
|
|
173
|
+
...entry,
|
|
174
|
+
refCount: entry.refCount - 1,
|
|
175
|
+
}),
|
|
176
|
+
}),
|
|
177
|
+
false,
|
|
178
|
+
'blobUrl/decrementRef'
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
hasBlobUrl: (key) => get().blobUrls.has(key),
|
|
184
|
+
|
|
185
|
+
// ========== Image Cache ==========
|
|
186
|
+
|
|
187
|
+
cacheDimensions: (src, dims) => {
|
|
188
|
+
set(
|
|
189
|
+
(state) => ({
|
|
190
|
+
imageDimensions: new Map(state.imageDimensions).set(src, dims),
|
|
191
|
+
}),
|
|
192
|
+
false,
|
|
193
|
+
'image/cacheDimensions'
|
|
194
|
+
);
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
getDimensions: (src) => get().imageDimensions.get(src) ?? null,
|
|
198
|
+
|
|
199
|
+
// ========== Audio Cache ==========
|
|
200
|
+
|
|
201
|
+
saveAudioPosition: (uri, time) => {
|
|
202
|
+
set(
|
|
203
|
+
(state) => ({
|
|
204
|
+
audioPlaybackPositions: new Map(state.audioPlaybackPositions).set(
|
|
205
|
+
uri,
|
|
206
|
+
time
|
|
207
|
+
),
|
|
208
|
+
}),
|
|
209
|
+
false,
|
|
210
|
+
'audio/savePosition'
|
|
211
|
+
);
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
getAudioPosition: (uri) =>
|
|
215
|
+
get().audioPlaybackPositions.get(uri) ?? null,
|
|
216
|
+
|
|
217
|
+
getEffectConfig: (key) => get().audioEffectConfigs.get(key) ?? null,
|
|
218
|
+
|
|
219
|
+
cacheEffectConfig: (key, config) => {
|
|
220
|
+
set(
|
|
221
|
+
(state) => ({
|
|
222
|
+
audioEffectConfigs: new Map(state.audioEffectConfigs).set(
|
|
223
|
+
key,
|
|
224
|
+
config
|
|
225
|
+
),
|
|
226
|
+
}),
|
|
227
|
+
false,
|
|
228
|
+
'audio/cacheEffectConfig'
|
|
229
|
+
);
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
// ========== Video Cache ==========
|
|
233
|
+
|
|
234
|
+
getOrCreateStreamUrl: (sessionId, path, generator) => {
|
|
235
|
+
const key = `${sessionId}:${path}`;
|
|
236
|
+
const cached = get().videoStreamUrls.get(key);
|
|
237
|
+
|
|
238
|
+
// Return if fresh
|
|
239
|
+
if (cached && Date.now() - cached.timestamp < STREAM_URL_TTL) {
|
|
240
|
+
return cached.url;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Generate and cache
|
|
244
|
+
const url = generator(sessionId, path);
|
|
245
|
+
set(
|
|
246
|
+
(state) => ({
|
|
247
|
+
videoStreamUrls: new Map(state.videoStreamUrls).set(key, {
|
|
248
|
+
url,
|
|
249
|
+
timestamp: Date.now(),
|
|
250
|
+
}),
|
|
251
|
+
}),
|
|
252
|
+
false,
|
|
253
|
+
'video/cacheStreamUrl'
|
|
254
|
+
);
|
|
255
|
+
return url;
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
getPosterUrl: (title) => get().videoPosterUrls.get(title) ?? null,
|
|
259
|
+
|
|
260
|
+
cachePosterUrl: (title, url) => {
|
|
261
|
+
set(
|
|
262
|
+
(state) => ({
|
|
263
|
+
videoPosterUrls: new Map(state.videoPosterUrls).set(title, url),
|
|
264
|
+
}),
|
|
265
|
+
false,
|
|
266
|
+
'video/cachePosterUrl'
|
|
267
|
+
);
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
saveVideoPosition: (key, time) => {
|
|
271
|
+
set(
|
|
272
|
+
(state) => ({
|
|
273
|
+
videoPlaybackPositions: new Map(state.videoPlaybackPositions).set(
|
|
274
|
+
key,
|
|
275
|
+
time
|
|
276
|
+
),
|
|
277
|
+
}),
|
|
278
|
+
false,
|
|
279
|
+
'video/savePosition'
|
|
280
|
+
);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
getVideoPosition: (key) =>
|
|
284
|
+
get().videoPlaybackPositions.get(key) ?? null,
|
|
285
|
+
|
|
286
|
+
cacheVideoMetadata: (url, meta) => {
|
|
287
|
+
set(
|
|
288
|
+
(state) => ({
|
|
289
|
+
videoMetadata: new Map(state.videoMetadata).set(url, meta),
|
|
290
|
+
}),
|
|
291
|
+
false,
|
|
292
|
+
'video/cacheMetadata'
|
|
293
|
+
);
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
getVideoMetadata: (url) => get().videoMetadata.get(url) ?? null,
|
|
297
|
+
|
|
298
|
+
invalidateSession: (sessionId) => {
|
|
299
|
+
set(
|
|
300
|
+
(state) => {
|
|
301
|
+
const newUrls = new Map(state.videoStreamUrls);
|
|
302
|
+
for (const [key] of newUrls) {
|
|
303
|
+
if (key.startsWith(`${sessionId}:`)) {
|
|
304
|
+
newUrls.delete(key);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return { videoStreamUrls: newUrls };
|
|
308
|
+
},
|
|
309
|
+
false,
|
|
310
|
+
'video/invalidateSession'
|
|
311
|
+
);
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
// ========== Global ==========
|
|
315
|
+
|
|
316
|
+
clearCache: () => {
|
|
317
|
+
// Revoke all blob URLs before clearing
|
|
318
|
+
get().blobUrls.forEach(({ url }) => URL.revokeObjectURL(url));
|
|
319
|
+
set(initialState, false, 'clearCache');
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
getCacheStats: () => ({
|
|
323
|
+
blobUrls: get().blobUrls.size,
|
|
324
|
+
dimensions: get().imageDimensions.size,
|
|
325
|
+
audioPositions: get().audioPlaybackPositions.size,
|
|
326
|
+
videoPositions: get().videoPlaybackPositions.size,
|
|
327
|
+
}),
|
|
328
|
+
}),
|
|
329
|
+
{
|
|
330
|
+
name: 'media-cache-storage',
|
|
331
|
+
// Only persist playback positions and poster URLs
|
|
332
|
+
partialize: (state) => ({
|
|
333
|
+
audioPlaybackPositions: Array.from(
|
|
334
|
+
state.audioPlaybackPositions.entries()
|
|
335
|
+
),
|
|
336
|
+
videoPlaybackPositions: Array.from(
|
|
337
|
+
state.videoPlaybackPositions.entries()
|
|
338
|
+
),
|
|
339
|
+
videoPosterUrls: Array.from(state.videoPosterUrls.entries()),
|
|
340
|
+
}),
|
|
341
|
+
// Rehydrate Maps from arrays
|
|
342
|
+
onRehydrateStorage: () => (state) => {
|
|
343
|
+
if (state) {
|
|
344
|
+
// Type assertion for persisted data
|
|
345
|
+
const persistedAudio = state.audioPlaybackPositions as unknown;
|
|
346
|
+
const persistedVideo = state.videoPlaybackPositions as unknown;
|
|
347
|
+
const persistedPosters = state.videoPosterUrls as unknown;
|
|
348
|
+
|
|
349
|
+
state.audioPlaybackPositions = new Map(
|
|
350
|
+
Array.isArray(persistedAudio)
|
|
351
|
+
? (persistedAudio as [string, number][])
|
|
352
|
+
: []
|
|
353
|
+
);
|
|
354
|
+
state.videoPlaybackPositions = new Map(
|
|
355
|
+
Array.isArray(persistedVideo)
|
|
356
|
+
? (persistedVideo as [string, number][])
|
|
357
|
+
: []
|
|
358
|
+
);
|
|
359
|
+
state.videoPosterUrls = new Map(
|
|
360
|
+
Array.isArray(persistedPosters)
|
|
361
|
+
? (persistedPosters as [string, string][])
|
|
362
|
+
: []
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
),
|
|
368
|
+
{ name: 'MediaCacheStore' }
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// ========== Selective Hooks ==========
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Hook for image-related cache operations
|
|
376
|
+
*/
|
|
377
|
+
export const useImageCache = () =>
|
|
378
|
+
useMediaCacheStore((state) => ({
|
|
379
|
+
getOrCreateBlobUrl: state.getOrCreateBlobUrl,
|
|
380
|
+
releaseBlobUrl: state.releaseBlobUrl,
|
|
381
|
+
hasBlobUrl: state.hasBlobUrl,
|
|
382
|
+
cacheDimensions: state.cacheDimensions,
|
|
383
|
+
getDimensions: state.getDimensions,
|
|
384
|
+
}));
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Hook for audio-related cache operations
|
|
388
|
+
*/
|
|
389
|
+
export const useAudioCache = () =>
|
|
390
|
+
useMediaCacheStore((state) => ({
|
|
391
|
+
getOrCreateBlobUrl: state.getOrCreateBlobUrl,
|
|
392
|
+
releaseBlobUrl: state.releaseBlobUrl,
|
|
393
|
+
hasBlobUrl: state.hasBlobUrl,
|
|
394
|
+
saveAudioPosition: state.saveAudioPosition,
|
|
395
|
+
getAudioPosition: state.getAudioPosition,
|
|
396
|
+
getEffectConfig: state.getEffectConfig,
|
|
397
|
+
cacheEffectConfig: state.cacheEffectConfig,
|
|
398
|
+
}));
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Hook for video-related cache operations
|
|
402
|
+
*/
|
|
403
|
+
export const useVideoCache = () =>
|
|
404
|
+
useMediaCacheStore((state) => ({
|
|
405
|
+
getOrCreateBlobUrl: state.getOrCreateBlobUrl,
|
|
406
|
+
releaseBlobUrl: state.releaseBlobUrl,
|
|
407
|
+
hasBlobUrl: state.hasBlobUrl,
|
|
408
|
+
getOrCreateStreamUrl: state.getOrCreateStreamUrl,
|
|
409
|
+
getPosterUrl: state.getPosterUrl,
|
|
410
|
+
cachePosterUrl: state.cachePosterUrl,
|
|
411
|
+
saveVideoPosition: state.saveVideoPosition,
|
|
412
|
+
getVideoPosition: state.getVideoPosition,
|
|
413
|
+
cacheVideoMetadata: state.cacheVideoMetadata,
|
|
414
|
+
getVideoMetadata: state.getVideoMetadata,
|
|
415
|
+
invalidateSession: state.invalidateSession,
|
|
416
|
+
}));
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Hook for blob URL cleanup on unmount
|
|
420
|
+
*/
|
|
421
|
+
export function useBlobUrlCleanup(key: string | null) {
|
|
422
|
+
const releaseBlobUrl = useMediaCacheStore((s) => s.releaseBlobUrl);
|
|
423
|
+
|
|
424
|
+
// Using inline effect to avoid importing useEffect in store
|
|
425
|
+
if (typeof window !== 'undefined') {
|
|
426
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
427
|
+
const { useEffect } = require('react');
|
|
428
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
429
|
+
useEffect(() => {
|
|
430
|
+
return () => {
|
|
431
|
+
if (key) {
|
|
432
|
+
releaseBlobUrl(key);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}, [key, releaseBlobUrl]);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ========== Utilities ==========
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Generate a cache key from ArrayBuffer content
|
|
443
|
+
* Uses first and last 1KB + length for fast hashing
|
|
444
|
+
*/
|
|
445
|
+
export function generateContentKey(content: ArrayBuffer): string {
|
|
446
|
+
const arr = new Uint8Array(content);
|
|
447
|
+
const len = arr.length;
|
|
448
|
+
|
|
449
|
+
// Take first 1KB
|
|
450
|
+
const start = arr.slice(0, Math.min(1024, len));
|
|
451
|
+
// Take last 1KB
|
|
452
|
+
const end = arr.slice(Math.max(0, len - 1024));
|
|
453
|
+
|
|
454
|
+
// Simple hash from bytes
|
|
455
|
+
let hash = len;
|
|
456
|
+
for (let i = 0; i < start.length; i++) {
|
|
457
|
+
hash = ((hash << 5) - hash + start[i]) | 0;
|
|
458
|
+
}
|
|
459
|
+
for (let i = 0; i < end.length; i++) {
|
|
460
|
+
hash = ((hash << 5) - hash + end[i]) | 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return `blob-${len}-${hash.toString(16)}`;
|
|
464
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# AudioPlayer Refactoring Plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Decompose the AudioPlayer module into a clean, maintainable folder structure with proper separation of concerns.
|
|
6
|
+
|
|
7
|
+
## Current Issues
|
|
8
|
+
|
|
9
|
+
1. **`context.tsx` is too large (574 lines)** - contains 4 different hooks and provider
|
|
10
|
+
2. **`types.ts` is monolithic (185 lines)** - mixes audio, component, and effect types
|
|
11
|
+
3. **`effects/index.ts` is overloaded (413 lines)** - types, constants, and calculations together
|
|
12
|
+
4. **`AudioReactiveCover.tsx` has inline effect components** - harder to maintain
|
|
13
|
+
|
|
14
|
+
## Target Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
AudioPlayer/
|
|
18
|
+
├── index.ts # Public API (re-exports)
|
|
19
|
+
│
|
|
20
|
+
├── types/
|
|
21
|
+
│ ├── index.ts # Re-exports all types
|
|
22
|
+
│ ├── audio.ts # Core audio types
|
|
23
|
+
│ ├── components.ts # Component props
|
|
24
|
+
│ └── effects.ts # Effect-related types
|
|
25
|
+
│
|
|
26
|
+
├── hooks/
|
|
27
|
+
│ ├── index.ts # Re-exports all hooks
|
|
28
|
+
│ ├── useSharedWebAudio.ts # Web Audio API management
|
|
29
|
+
│ ├── useAudioAnalysis.ts # Frequency analysis
|
|
30
|
+
│ ├── useAudioHotkeys.ts # Keyboard shortcuts (existing)
|
|
31
|
+
│ └── useVisualization.tsx # Settings + Provider (renamed)
|
|
32
|
+
│
|
|
33
|
+
├── context/
|
|
34
|
+
│ ├── index.ts # Re-exports
|
|
35
|
+
│ ├── AudioProvider.tsx # Provider component only
|
|
36
|
+
│ └── selectors.ts # useAudio, useAudioControls, useAudioState, useAudioElement
|
|
37
|
+
│
|
|
38
|
+
├── components/
|
|
39
|
+
│ ├── index.ts # Re-exports
|
|
40
|
+
│ ├── AudioPlayer.tsx # Main player UI
|
|
41
|
+
│ ├── AudioEqualizer.tsx # Equalizer visualization
|
|
42
|
+
│ ├── SimpleAudioPlayer.tsx # Easy-to-use wrapper
|
|
43
|
+
│ ├── ShortcutsPopover.tsx # Keyboard shortcuts popover
|
|
44
|
+
│ ├── VisualizationToggle.tsx # Effect toggle button
|
|
45
|
+
│ └── ReactiveCover/ # Cover with effects
|
|
46
|
+
│ ├── index.tsx # Main component
|
|
47
|
+
│ ├── GlowEffect.tsx # Glow effect
|
|
48
|
+
│ ├── OrbsEffect.tsx # Orbs effect
|
|
49
|
+
│ ├── SpotlightEffect.tsx # Spotlight effect
|
|
50
|
+
│ └── MeshEffect.tsx # Mesh gradient effect
|
|
51
|
+
│
|
|
52
|
+
├── effects/
|
|
53
|
+
│ ├── index.ts # Re-exports
|
|
54
|
+
│ ├── constants.ts # INTENSITY_CONFIG, COLOR_SCHEMES
|
|
55
|
+
│ ├── calculations.ts # calculateGlowLayers, calculateOrbs, etc.
|
|
56
|
+
│ └── animations.ts # EFFECT_ANIMATIONS CSS string
|
|
57
|
+
│
|
|
58
|
+
└── utils/
|
|
59
|
+
├── index.ts # Re-exports
|
|
60
|
+
└── formatTime.ts # Time formatting helper
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Migration Steps
|
|
64
|
+
|
|
65
|
+
### Phase 1: Create folder structure
|
|
66
|
+
- [ ] Create `types/`, `hooks/`, `context/`, `components/`, `effects/`, `utils/` folders
|
|
67
|
+
|
|
68
|
+
### Phase 2: Split types
|
|
69
|
+
- [ ] Create `types/audio.ts` - AudioSource, PlaybackStatus, SharedWebAudioContext, AudioContextState
|
|
70
|
+
- [ ] Create `types/components.ts` - AudioPlayerProps, AudioEqualizerProps, etc.
|
|
71
|
+
- [ ] Create `types/effects.ts` - EffectVariant, AudioLevels, EffectConfig, etc.
|
|
72
|
+
- [ ] Create `types/index.ts` - re-export all
|
|
73
|
+
|
|
74
|
+
### Phase 3: Extract hooks from context.tsx
|
|
75
|
+
- [ ] Create `hooks/useSharedWebAudio.ts` - extract from context.tsx
|
|
76
|
+
- [ ] Create `hooks/useAudioAnalysis.ts` - extract from context.tsx
|
|
77
|
+
- [ ] Move `useAudioHotkeys.ts` to `hooks/`
|
|
78
|
+
- [ ] Move `useAudioVisualization.tsx` to `hooks/useVisualization.tsx`
|
|
79
|
+
- [ ] Create `hooks/index.ts`
|
|
80
|
+
|
|
81
|
+
### Phase 4: Refactor context
|
|
82
|
+
- [ ] Create `context/AudioProvider.tsx` - provider only
|
|
83
|
+
- [ ] Create `context/selectors.ts` - useAudio, useAudioControls, useAudioState, useAudioElement
|
|
84
|
+
- [ ] Create `context/index.ts`
|
|
85
|
+
|
|
86
|
+
### Phase 5: Reorganize components
|
|
87
|
+
- [ ] Move `AudioPlayer.tsx` to `components/`
|
|
88
|
+
- [ ] Move `AudioEqualizer.tsx` to `components/`
|
|
89
|
+
- [ ] Move `SimpleAudioPlayer.tsx` to `components/`
|
|
90
|
+
- [ ] Rename and move `AudioShortcutsPopover.tsx` to `components/ShortcutsPopover.tsx`
|
|
91
|
+
- [ ] Move `VisualizationToggle.tsx` to `components/`
|
|
92
|
+
- [ ] Split `AudioReactiveCover.tsx` into `components/ReactiveCover/`
|
|
93
|
+
|
|
94
|
+
### Phase 6: Refactor effects
|
|
95
|
+
- [ ] Create `effects/constants.ts` - INTENSITY_CONFIG, COLOR_SCHEMES
|
|
96
|
+
- [ ] Create `effects/calculations.ts` - all calculate* functions
|
|
97
|
+
- [ ] Create `effects/animations.ts` - EFFECT_ANIMATIONS
|
|
98
|
+
- [ ] Update `effects/index.ts`
|
|
99
|
+
|
|
100
|
+
### Phase 7: Create utils
|
|
101
|
+
- [ ] Create `utils/formatTime.ts` - extract from AudioPlayer.tsx
|
|
102
|
+
- [ ] Create `utils/index.ts`
|
|
103
|
+
|
|
104
|
+
### Phase 8: Update main index.ts
|
|
105
|
+
- [ ] Update `index.ts` to re-export from new locations
|
|
106
|
+
- [ ] Ensure backward compatibility of public API
|
|
107
|
+
|
|
108
|
+
### Phase 9: Cleanup
|
|
109
|
+
- [ ] Delete old files
|
|
110
|
+
- [ ] Run type check
|
|
111
|
+
- [ ] Test imports
|
|
112
|
+
|
|
113
|
+
## File Size Targets
|
|
114
|
+
|
|
115
|
+
| File | Current | Target |
|
|
116
|
+
|------|---------|--------|
|
|
117
|
+
| context.tsx | 574 lines | Split into 4 files (~100-150 each) |
|
|
118
|
+
| types.ts | 185 lines | Split into 3 files (~60 each) |
|
|
119
|
+
| effects/index.ts | 413 lines | Split into 3 files (~100-150 each) |
|
|
120
|
+
| AudioReactiveCover.tsx | 390 lines | Split into 5 files (~80 each) |
|
|
121
|
+
|
|
122
|
+
## Public API (must remain unchanged)
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Components
|
|
126
|
+
export { SimpleAudioPlayer } from './components';
|
|
127
|
+
export { AudioPlayer } from './components';
|
|
128
|
+
export { AudioEqualizer } from './components';
|
|
129
|
+
export { AudioReactiveCover } from './components';
|
|
130
|
+
export { AudioShortcutsPopover } from './components'; // alias for backward compat
|
|
131
|
+
|
|
132
|
+
// Context & Hooks
|
|
133
|
+
export { AudioProvider, useAudio, useAudioControls, useAudioState, useAudioElement } from './context';
|
|
134
|
+
export { useAudioHotkeys, AUDIO_SHORTCUTS } from './hooks';
|
|
135
|
+
export { useAudioVisualization, VisualizationProvider } from './hooks'; // alias
|
|
136
|
+
|
|
137
|
+
// Effects utilities
|
|
138
|
+
export { INTENSITY_CONFIG, COLOR_SCHEMES, ... } from './effects';
|
|
139
|
+
|
|
140
|
+
// Types (all existing types)
|
|
141
|
+
export type { AudioSource, PlaybackStatus, ... } from './types';
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Notes
|
|
145
|
+
|
|
146
|
+
- Keep backward compatibility for all public exports
|
|
147
|
+
- Internal imports will change but external API stays the same
|
|
148
|
+
- Run `pnpm check` after each phase to catch issues early
|