@djangocfg/ui-nextjs 2.1.66 → 2.1.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -6
- package/src/stores/index.ts +8 -0
- package/src/stores/mediaCache.ts +474 -0
- package/src/tools/AudioPlayer/@refactoring/00-PLAN.md +148 -0
- package/src/tools/AudioPlayer/@refactoring/01-TYPES.md +301 -0
- package/src/tools/AudioPlayer/@refactoring/02-HOOKS.md +281 -0
- package/src/tools/AudioPlayer/@refactoring/03-CONTEXT.md +328 -0
- package/src/tools/AudioPlayer/@refactoring/04-COMPONENTS.md +251 -0
- package/src/tools/AudioPlayer/@refactoring/05-EFFECTS.md +427 -0
- package/src/tools/AudioPlayer/@refactoring/06-UTILS-AND-INDEX.md +193 -0
- package/src/tools/AudioPlayer/@refactoring/07-EXECUTION-CHECKLIST.md +146 -0
- package/src/tools/AudioPlayer/README.md +35 -11
- package/src/tools/AudioPlayer/{AudioEqualizer.tsx → components/AudioEqualizer.tsx} +29 -64
- package/src/tools/AudioPlayer/{AudioPlayer.tsx → components/AudioPlayer.tsx} +22 -14
- package/src/tools/AudioPlayer/{AudioShortcutsPopover.tsx → components/AudioShortcutsPopover.tsx} +6 -2
- package/src/tools/AudioPlayer/components/ReactiveCover/AudioReactiveCover.tsx +147 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/GlowEffect.tsx +110 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/MeshEffect.tsx +58 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/OrbsEffect.tsx +45 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/SpotlightEffect.tsx +82 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/effects/index.ts +8 -0
- package/src/tools/AudioPlayer/components/ReactiveCover/index.ts +6 -0
- package/src/tools/AudioPlayer/{SimpleAudioPlayer.tsx → components/SimpleAudioPlayer.tsx} +12 -7
- package/src/tools/AudioPlayer/{VisualizationToggle.tsx → components/VisualizationToggle.tsx} +2 -6
- package/src/tools/AudioPlayer/components/index.ts +21 -0
- package/src/tools/AudioPlayer/context/AudioProvider.tsx +292 -0
- package/src/tools/AudioPlayer/context/index.ts +11 -0
- package/src/tools/AudioPlayer/context/selectors.ts +96 -0
- package/src/tools/AudioPlayer/hooks/index.ts +29 -0
- package/src/tools/AudioPlayer/hooks/useAudioAnalysis.ts +110 -0
- package/src/tools/AudioPlayer/{useAudioHotkeys.ts → hooks/useAudioHotkeys.ts} +11 -4
- package/src/tools/AudioPlayer/hooks/useSharedWebAudio.ts +106 -0
- package/src/tools/AudioPlayer/{useAudioVisualization.tsx → hooks/useVisualization.tsx} +11 -5
- package/src/tools/AudioPlayer/index.ts +104 -49
- package/src/tools/AudioPlayer/types/audio.ts +107 -0
- package/src/tools/AudioPlayer/{types.ts → types/components.ts} +20 -84
- package/src/tools/AudioPlayer/types/effects.ts +73 -0
- package/src/tools/AudioPlayer/types/index.ts +35 -0
- package/src/tools/AudioPlayer/utils/formatTime.ts +10 -0
- package/src/tools/AudioPlayer/utils/index.ts +5 -0
- package/src/tools/ImageViewer/@refactoring/00-PLAN.md +71 -0
- package/src/tools/ImageViewer/@refactoring/01-TYPES.md +121 -0
- package/src/tools/ImageViewer/@refactoring/02-UTILS.md +143 -0
- package/src/tools/ImageViewer/@refactoring/03-HOOKS.md +261 -0
- package/src/tools/ImageViewer/@refactoring/04-COMPONENTS.md +427 -0
- package/src/tools/ImageViewer/@refactoring/05-EXECUTION-CHECKLIST.md +126 -0
- package/src/tools/ImageViewer/README.md +16 -3
- package/src/tools/ImageViewer/components/ImageInfo.tsx +44 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +150 -0
- package/src/tools/ImageViewer/components/ImageViewer.tsx +235 -0
- package/src/tools/ImageViewer/components/index.ts +7 -0
- package/src/tools/ImageViewer/hooks/index.ts +9 -0
- package/src/tools/ImageViewer/hooks/useImageLoading.ts +153 -0
- package/src/tools/ImageViewer/hooks/useImageTransform.ts +101 -0
- package/src/tools/ImageViewer/index.ts +47 -3
- package/src/tools/ImageViewer/types.ts +75 -0
- package/src/tools/ImageViewer/utils/constants.ts +59 -0
- package/src/tools/ImageViewer/utils/index.ts +16 -0
- package/src/tools/ImageViewer/utils/lqip.ts +47 -0
- package/src/tools/VideoPlayer/@refactoring/00-PLAN.md +91 -0
- package/src/tools/VideoPlayer/@refactoring/01-TYPES.md +284 -0
- package/src/tools/VideoPlayer/@refactoring/02-UTILS.md +141 -0
- package/src/tools/VideoPlayer/@refactoring/03-HOOKS.md +178 -0
- package/src/tools/VideoPlayer/@refactoring/04-COMPONENTS.md +95 -0
- package/src/tools/VideoPlayer/@refactoring/05-EXECUTION-CHECKLIST.md +139 -0
- package/src/tools/VideoPlayer/README.md +26 -10
- package/src/tools/VideoPlayer/{VideoControls.tsx → components/VideoControls.tsx} +8 -9
- package/src/tools/VideoPlayer/{VideoErrorFallback.tsx → components/VideoErrorFallback.tsx} +2 -2
- package/src/tools/VideoPlayer/{VideoPlayer.tsx → components/VideoPlayer.tsx} +4 -5
- package/src/tools/VideoPlayer/components/index.ts +14 -0
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +52 -0
- package/src/tools/VideoPlayer/context/index.ts +8 -0
- package/src/tools/VideoPlayer/hooks/index.ts +9 -0
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +109 -0
- package/src/tools/VideoPlayer/index.ts +29 -20
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +118 -28
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +89 -11
- package/src/tools/VideoPlayer/types/index.ts +38 -0
- package/src/tools/VideoPlayer/types/player.ts +116 -0
- package/src/tools/VideoPlayer/types/provider.ts +93 -0
- package/src/tools/VideoPlayer/types/sources.ts +97 -0
- package/src/tools/VideoPlayer/utils/fileSource.ts +78 -0
- package/src/tools/VideoPlayer/utils/index.ts +11 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +75 -0
- package/src/tools/index.ts +10 -0
- package/src/tools/AudioPlayer/AudioReactiveCover.tsx +0 -389
- package/src/tools/AudioPlayer/context.tsx +0 -426
- package/src/tools/ImageViewer/ImageViewer.tsx +0 -416
- package/src/tools/VideoPlayer/VideoPlayerContext.tsx +0 -125
- package/src/tools/VideoPlayer/types.ts +0 -367
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ImageViewer - Image viewer with zoom, pan, rotate, flip
|
|
5
|
-
*
|
|
6
|
-
* Features:
|
|
7
|
-
* - Zoom with mouse wheel and presets
|
|
8
|
-
* - Pan with drag
|
|
9
|
-
* - Rotate 90°
|
|
10
|
-
* - Flip horizontal/vertical
|
|
11
|
-
* - Fullscreen dialog
|
|
12
|
-
* - Keyboard shortcuts (+/-, 0, r)
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
|
16
|
-
import { ImageIcon, ZoomIn, ZoomOut, RotateCw, FlipHorizontal, FlipVertical, Maximize2, Expand } from 'lucide-react';
|
|
17
|
-
import { TransformWrapper, TransformComponent, useControls } from 'react-zoom-pan-pinch';
|
|
18
|
-
import { Button, cn, Dialog, DialogContent } from '@djangocfg/ui-core';
|
|
19
|
-
import {
|
|
20
|
-
DropdownMenu,
|
|
21
|
-
DropdownMenuContent,
|
|
22
|
-
DropdownMenuItem,
|
|
23
|
-
DropdownMenuTrigger,
|
|
24
|
-
} from '../../components/dropdown-menu';
|
|
25
|
-
|
|
26
|
-
// =============================================================================
|
|
27
|
-
// TYPES
|
|
28
|
-
// =============================================================================
|
|
29
|
-
|
|
30
|
-
export interface ImageFile {
|
|
31
|
-
name: string;
|
|
32
|
-
path: string;
|
|
33
|
-
mimeType?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Preset zoom levels
|
|
37
|
-
const ZOOM_PRESETS = [
|
|
38
|
-
{ label: 'Fit', value: 'fit' },
|
|
39
|
-
{ label: '25%', value: 0.25 },
|
|
40
|
-
{ label: '50%', value: 0.5 },
|
|
41
|
-
{ label: '100%', value: 1 },
|
|
42
|
-
{ label: '200%', value: 2 },
|
|
43
|
-
{ label: '400%', value: 4 },
|
|
44
|
-
] as const;
|
|
45
|
-
|
|
46
|
-
export interface ImageViewerProps {
|
|
47
|
-
/** File info (name, path, mimeType) */
|
|
48
|
-
file: ImageFile;
|
|
49
|
-
/** Image content as string (data URL or base64) or ArrayBuffer */
|
|
50
|
-
content: string | ArrayBuffer;
|
|
51
|
-
/** Hide expand button when already in dialog */
|
|
52
|
-
inDialog?: boolean;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
interface ImageTransform {
|
|
56
|
-
rotation: number;
|
|
57
|
-
flipH: boolean;
|
|
58
|
-
flipV: boolean;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Toolbar component
|
|
62
|
-
function ImageToolbar({
|
|
63
|
-
scale,
|
|
64
|
-
transform,
|
|
65
|
-
onRotate,
|
|
66
|
-
onFlipH,
|
|
67
|
-
onFlipV,
|
|
68
|
-
onZoomPreset,
|
|
69
|
-
onExpand,
|
|
70
|
-
}: {
|
|
71
|
-
scale: number;
|
|
72
|
-
transform: ImageTransform;
|
|
73
|
-
onRotate: () => void;
|
|
74
|
-
onFlipH: () => void;
|
|
75
|
-
onFlipV: () => void;
|
|
76
|
-
onZoomPreset: (value: number | 'fit') => void;
|
|
77
|
-
onExpand?: () => void;
|
|
78
|
-
}) {
|
|
79
|
-
const { zoomIn, zoomOut, resetTransform, centerView } = useControls();
|
|
80
|
-
|
|
81
|
-
const zoomLabel = useMemo(() => {
|
|
82
|
-
const percent = Math.round(scale * 100);
|
|
83
|
-
return `${percent}%`;
|
|
84
|
-
}, [scale]);
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 flex items-center gap-0.5 bg-background/90 backdrop-blur-sm border rounded-lg p-1 shadow-lg">
|
|
88
|
-
{/* Zoom controls */}
|
|
89
|
-
<Button
|
|
90
|
-
variant="ghost"
|
|
91
|
-
size="icon"
|
|
92
|
-
className="h-7 w-7"
|
|
93
|
-
onClick={() => zoomOut()}
|
|
94
|
-
>
|
|
95
|
-
<ZoomOut className="h-3.5 w-3.5" />
|
|
96
|
-
</Button>
|
|
97
|
-
|
|
98
|
-
<DropdownMenu>
|
|
99
|
-
<DropdownMenuTrigger asChild>
|
|
100
|
-
<Button variant="ghost" size="sm" className="h-7 px-2 min-w-[52px] font-mono text-xs">
|
|
101
|
-
{zoomLabel}
|
|
102
|
-
</Button>
|
|
103
|
-
</DropdownMenuTrigger>
|
|
104
|
-
<DropdownMenuContent align="center" className="min-w-[80px]">
|
|
105
|
-
{ZOOM_PRESETS.map((preset) => (
|
|
106
|
-
<DropdownMenuItem
|
|
107
|
-
key={preset.label}
|
|
108
|
-
onClick={() => onZoomPreset(preset.value)}
|
|
109
|
-
className="text-xs justify-center"
|
|
110
|
-
>
|
|
111
|
-
{preset.label}
|
|
112
|
-
</DropdownMenuItem>
|
|
113
|
-
))}
|
|
114
|
-
</DropdownMenuContent>
|
|
115
|
-
</DropdownMenu>
|
|
116
|
-
|
|
117
|
-
<Button
|
|
118
|
-
variant="ghost"
|
|
119
|
-
size="icon"
|
|
120
|
-
className="h-7 w-7"
|
|
121
|
-
onClick={() => zoomIn()}
|
|
122
|
-
>
|
|
123
|
-
<ZoomIn className="h-3.5 w-3.5" />
|
|
124
|
-
</Button>
|
|
125
|
-
|
|
126
|
-
<div className="w-px h-4 bg-border mx-1" />
|
|
127
|
-
|
|
128
|
-
{/* Fit to view */}
|
|
129
|
-
<Button
|
|
130
|
-
variant="ghost"
|
|
131
|
-
size="icon"
|
|
132
|
-
className="h-7 w-7"
|
|
133
|
-
onClick={() => resetTransform()}
|
|
134
|
-
title="Fit to view"
|
|
135
|
-
>
|
|
136
|
-
<Maximize2 className="h-3.5 w-3.5" />
|
|
137
|
-
</Button>
|
|
138
|
-
|
|
139
|
-
<div className="w-px h-4 bg-border mx-1" />
|
|
140
|
-
|
|
141
|
-
{/* Transform controls */}
|
|
142
|
-
<Button
|
|
143
|
-
variant="ghost"
|
|
144
|
-
size="icon"
|
|
145
|
-
className={cn("h-7 w-7", transform.flipH && "bg-accent")}
|
|
146
|
-
onClick={onFlipH}
|
|
147
|
-
title="Flip horizontal"
|
|
148
|
-
>
|
|
149
|
-
<FlipHorizontal className="h-3.5 w-3.5" />
|
|
150
|
-
</Button>
|
|
151
|
-
|
|
152
|
-
<Button
|
|
153
|
-
variant="ghost"
|
|
154
|
-
size="icon"
|
|
155
|
-
className={cn("h-7 w-7", transform.flipV && "bg-accent")}
|
|
156
|
-
onClick={onFlipV}
|
|
157
|
-
title="Flip vertical"
|
|
158
|
-
>
|
|
159
|
-
<FlipVertical className="h-3.5 w-3.5" />
|
|
160
|
-
</Button>
|
|
161
|
-
|
|
162
|
-
<Button
|
|
163
|
-
variant="ghost"
|
|
164
|
-
size="icon"
|
|
165
|
-
className="h-7 w-7"
|
|
166
|
-
onClick={onRotate}
|
|
167
|
-
title="Rotate 90°"
|
|
168
|
-
>
|
|
169
|
-
<RotateCw className="h-3.5 w-3.5" />
|
|
170
|
-
</Button>
|
|
171
|
-
|
|
172
|
-
{transform.rotation !== 0 && (
|
|
173
|
-
<span className="text-[10px] text-muted-foreground font-mono pl-1">
|
|
174
|
-
{transform.rotation}°
|
|
175
|
-
</span>
|
|
176
|
-
)}
|
|
177
|
-
|
|
178
|
-
{onExpand && (
|
|
179
|
-
<>
|
|
180
|
-
<div className="w-px h-4 bg-border mx-1" />
|
|
181
|
-
<Button
|
|
182
|
-
variant="ghost"
|
|
183
|
-
size="icon"
|
|
184
|
-
className="h-7 w-7"
|
|
185
|
-
onClick={onExpand}
|
|
186
|
-
title="Open in fullscreen"
|
|
187
|
-
>
|
|
188
|
-
<Expand className="h-3.5 w-3.5" />
|
|
189
|
-
</Button>
|
|
190
|
-
</>
|
|
191
|
-
)}
|
|
192
|
-
</div>
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Image dimensions badge
|
|
197
|
-
function ImageInfo({ src }: { src: string }) {
|
|
198
|
-
const [dimensions, setDimensions] = useState<{ w: number; h: number } | null>(null);
|
|
199
|
-
|
|
200
|
-
useEffect(() => {
|
|
201
|
-
const img = new Image();
|
|
202
|
-
img.onload = () => setDimensions({ w: img.naturalWidth, h: img.naturalHeight });
|
|
203
|
-
img.src = src;
|
|
204
|
-
}, [src]);
|
|
205
|
-
|
|
206
|
-
if (!dimensions) return null;
|
|
207
|
-
|
|
208
|
-
return (
|
|
209
|
-
<div className="absolute top-3 right-3 z-10 px-2 py-1 bg-background/80 backdrop-blur-sm border rounded text-[10px] text-muted-foreground font-mono">
|
|
210
|
-
{dimensions.w} × {dimensions.h}
|
|
211
|
-
</div>
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function ImageViewer({ file, content, inDialog = false }: ImageViewerProps) {
|
|
216
|
-
const contentSize = content ? (typeof content === 'string' ? content.length : content.byteLength) : 0;
|
|
217
|
-
const hasContent = contentSize > 0;
|
|
218
|
-
|
|
219
|
-
const [transform, setTransform] = useState<ImageTransform>({
|
|
220
|
-
rotation: 0,
|
|
221
|
-
flipH: false,
|
|
222
|
-
flipV: false,
|
|
223
|
-
});
|
|
224
|
-
const [scale, setScale] = useState(1);
|
|
225
|
-
const [dialogOpen, setDialogOpen] = useState(false);
|
|
226
|
-
const blobUrlRef = useRef<string | null>(null);
|
|
227
|
-
const [src, setSrc] = useState<string | null>(null);
|
|
228
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
229
|
-
const controlsRef = useRef<ReturnType<typeof useControls> | null>(null);
|
|
230
|
-
|
|
231
|
-
// Create blob URL
|
|
232
|
-
useEffect(() => {
|
|
233
|
-
if (!hasContent) {
|
|
234
|
-
setSrc(null);
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (typeof content === 'string') {
|
|
239
|
-
const url = content.startsWith('data:') ? content : `data:${file.mimeType};base64,${btoa(content)}`;
|
|
240
|
-
setSrc(url);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const blob = new Blob([content], { type: file.mimeType || 'image/png' });
|
|
245
|
-
const url = URL.createObjectURL(blob);
|
|
246
|
-
blobUrlRef.current = url;
|
|
247
|
-
setSrc(url);
|
|
248
|
-
|
|
249
|
-
return () => {
|
|
250
|
-
if (blobUrlRef.current) {
|
|
251
|
-
URL.revokeObjectURL(blobUrlRef.current);
|
|
252
|
-
blobUrlRef.current = null;
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
}, [content, file.mimeType, hasContent]);
|
|
256
|
-
|
|
257
|
-
// Reset transform when file changes
|
|
258
|
-
useEffect(() => {
|
|
259
|
-
setTransform({ rotation: 0, flipH: false, flipV: false });
|
|
260
|
-
}, [file.path]);
|
|
261
|
-
|
|
262
|
-
const handleRotate = useCallback(() => {
|
|
263
|
-
setTransform((prev) => ({ ...prev, rotation: (prev.rotation + 90) % 360 }));
|
|
264
|
-
}, []);
|
|
265
|
-
|
|
266
|
-
const handleFlipH = useCallback(() => {
|
|
267
|
-
setTransform((prev) => ({ ...prev, flipH: !prev.flipH }));
|
|
268
|
-
}, []);
|
|
269
|
-
|
|
270
|
-
const handleFlipV = useCallback(() => {
|
|
271
|
-
setTransform((prev) => ({ ...prev, flipV: !prev.flipV }));
|
|
272
|
-
}, []);
|
|
273
|
-
|
|
274
|
-
const handleZoomPreset = useCallback((value: number | 'fit') => {
|
|
275
|
-
if (!controlsRef.current) return;
|
|
276
|
-
if (value === 'fit') {
|
|
277
|
-
controlsRef.current.resetTransform();
|
|
278
|
-
} else {
|
|
279
|
-
controlsRef.current.setTransform(0, 0, value);
|
|
280
|
-
}
|
|
281
|
-
}, []);
|
|
282
|
-
|
|
283
|
-
const handleExpand = useCallback(() => {
|
|
284
|
-
setDialogOpen(true);
|
|
285
|
-
}, []);
|
|
286
|
-
|
|
287
|
-
// CSS transform for rotation and flip
|
|
288
|
-
const imageTransform = useMemo(() => {
|
|
289
|
-
const transforms: string[] = [];
|
|
290
|
-
if (transform.rotation) transforms.push(`rotate(${transform.rotation}deg)`);
|
|
291
|
-
if (transform.flipH) transforms.push('scaleX(-1)');
|
|
292
|
-
if (transform.flipV) transforms.push('scaleY(-1)');
|
|
293
|
-
return transforms.join(' ') || 'none';
|
|
294
|
-
}, [transform]);
|
|
295
|
-
|
|
296
|
-
// Keyboard shortcuts
|
|
297
|
-
useEffect(() => {
|
|
298
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
299
|
-
if (!containerRef.current?.contains(document.activeElement) &&
|
|
300
|
-
document.activeElement !== containerRef.current) {
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const controls = controlsRef.current;
|
|
305
|
-
if (!controls) return;
|
|
306
|
-
|
|
307
|
-
switch (e.key) {
|
|
308
|
-
case '+':
|
|
309
|
-
case '=':
|
|
310
|
-
e.preventDefault();
|
|
311
|
-
controls.zoomIn();
|
|
312
|
-
break;
|
|
313
|
-
case '-':
|
|
314
|
-
e.preventDefault();
|
|
315
|
-
controls.zoomOut();
|
|
316
|
-
break;
|
|
317
|
-
case '0':
|
|
318
|
-
e.preventDefault();
|
|
319
|
-
controls.resetTransform();
|
|
320
|
-
break;
|
|
321
|
-
case 'r':
|
|
322
|
-
if (!e.metaKey && !e.ctrlKey) {
|
|
323
|
-
e.preventDefault();
|
|
324
|
-
handleRotate();
|
|
325
|
-
}
|
|
326
|
-
break;
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
window.addEventListener('keydown', handleKeyDown);
|
|
331
|
-
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
332
|
-
}, [handleRotate]);
|
|
333
|
-
|
|
334
|
-
if (!hasContent) {
|
|
335
|
-
return (
|
|
336
|
-
<div className="flex-1 flex flex-col items-center justify-center gap-2 bg-muted/30">
|
|
337
|
-
<ImageIcon className="w-12 h-12 text-muted-foreground/50" />
|
|
338
|
-
<p className="text-sm text-muted-foreground">Failed to load image</p>
|
|
339
|
-
</div>
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return (
|
|
344
|
-
<div
|
|
345
|
-
ref={containerRef}
|
|
346
|
-
tabIndex={0}
|
|
347
|
-
className={cn(
|
|
348
|
-
"flex-1 h-full relative overflow-hidden outline-none",
|
|
349
|
-
"bg-[length:16px_16px]",
|
|
350
|
-
"[background-color:hsl(var(--muted)/0.2)]",
|
|
351
|
-
"[background-image:linear-gradient(45deg,hsl(var(--muted)/0.4)_25%,transparent_25%),linear-gradient(-45deg,hsl(var(--muted)/0.4)_25%,transparent_25%),linear-gradient(45deg,transparent_75%,hsl(var(--muted)/0.4)_75%),linear-gradient(-45deg,transparent_75%,hsl(var(--muted)/0.4)_75%)]",
|
|
352
|
-
"[background-position:0_0,0_8px,8px_-8px,-8px_0px]"
|
|
353
|
-
)}
|
|
354
|
-
>
|
|
355
|
-
{src && <ImageInfo src={src} />}
|
|
356
|
-
|
|
357
|
-
<TransformWrapper
|
|
358
|
-
initialScale={1}
|
|
359
|
-
minScale={0.1}
|
|
360
|
-
maxScale={8}
|
|
361
|
-
centerOnInit
|
|
362
|
-
onTransformed={(ref, state) => {
|
|
363
|
-
setScale(state.scale);
|
|
364
|
-
controlsRef.current = ref;
|
|
365
|
-
}}
|
|
366
|
-
onInit={(ref) => {
|
|
367
|
-
controlsRef.current = ref;
|
|
368
|
-
}}
|
|
369
|
-
wheel={{ step: 0.1 }}
|
|
370
|
-
doubleClick={{ mode: 'toggle', step: 2 }}
|
|
371
|
-
panning={{ velocityDisabled: false }}
|
|
372
|
-
>
|
|
373
|
-
<ImageToolbar
|
|
374
|
-
scale={scale}
|
|
375
|
-
transform={transform}
|
|
376
|
-
onRotate={handleRotate}
|
|
377
|
-
onFlipH={handleFlipH}
|
|
378
|
-
onFlipV={handleFlipV}
|
|
379
|
-
onZoomPreset={handleZoomPreset}
|
|
380
|
-
onExpand={!inDialog ? handleExpand : undefined}
|
|
381
|
-
/>
|
|
382
|
-
|
|
383
|
-
<TransformComponent
|
|
384
|
-
wrapperClass="!w-full !h-full cursor-grab active:cursor-grabbing"
|
|
385
|
-
contentClass="!w-full !h-full flex items-center justify-center"
|
|
386
|
-
>
|
|
387
|
-
<img
|
|
388
|
-
src={src!}
|
|
389
|
-
alt={file.name}
|
|
390
|
-
className="max-w-full max-h-full object-contain select-none"
|
|
391
|
-
style={{ transform: imageTransform, transition: 'transform 0.15s ease-out' }}
|
|
392
|
-
draggable={false}
|
|
393
|
-
/>
|
|
394
|
-
</TransformComponent>
|
|
395
|
-
</TransformWrapper>
|
|
396
|
-
|
|
397
|
-
{/* Fullscreen dialog */}
|
|
398
|
-
{!inDialog && (
|
|
399
|
-
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
400
|
-
<DialogContent className="max-w-[95vw] max-h-[95vh] w-[95vw] h-[95vh] p-0 overflow-hidden [&>button]:hidden">
|
|
401
|
-
<div className="h-full flex flex-col">
|
|
402
|
-
<div className="flex items-center justify-between px-4 py-2 border-b shrink-0">
|
|
403
|
-
<span className="text-sm font-medium truncate">{file.name}</span>
|
|
404
|
-
</div>
|
|
405
|
-
<div className="flex-1 min-h-0 relative">
|
|
406
|
-
<div className="absolute inset-0">
|
|
407
|
-
<ImageViewer file={file} content={content} inDialog />
|
|
408
|
-
</div>
|
|
409
|
-
</div>
|
|
410
|
-
</div>
|
|
411
|
-
</DialogContent>
|
|
412
|
-
</Dialog>
|
|
413
|
-
)}
|
|
414
|
-
</div>
|
|
415
|
-
);
|
|
416
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VideoPlayerContext - Context for streaming configuration
|
|
3
|
-
* Simplifies streaming API by providing getStreamUrl globally
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use client';
|
|
7
|
-
|
|
8
|
-
import React, { createContext, useContext, useMemo } from 'react';
|
|
9
|
-
|
|
10
|
-
import type { VideoSourceUnion, StreamSource } from './types';
|
|
11
|
-
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// Context Types
|
|
14
|
-
// ============================================================================
|
|
15
|
-
|
|
16
|
-
export interface VideoPlayerContextValue {
|
|
17
|
-
/** Function to generate stream URL (for HTTP Range streaming) */
|
|
18
|
-
getStreamUrl?: (sessionId: string, path: string) => string;
|
|
19
|
-
/** Current session ID */
|
|
20
|
-
sessionId?: string | null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface VideoPlayerProviderProps extends VideoPlayerContextValue {
|
|
24
|
-
children: React.ReactNode;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ============================================================================
|
|
28
|
-
// Context
|
|
29
|
-
// ============================================================================
|
|
30
|
-
|
|
31
|
-
const VideoPlayerContext = createContext<VideoPlayerContextValue | null>(null);
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Provider for VideoPlayer streaming configuration
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* // In your app layout or FileWorkspace
|
|
38
|
-
* <VideoPlayerProvider
|
|
39
|
-
* sessionId={sessionId}
|
|
40
|
-
* getStreamUrl={terminalClient.terminal_media.streamStreamRetrieveUrl}
|
|
41
|
-
* >
|
|
42
|
-
* <VideoPlayer source={{ type: 'stream', path: '/video.mp4' }} />
|
|
43
|
-
* </VideoPlayerProvider>
|
|
44
|
-
*/
|
|
45
|
-
export function VideoPlayerProvider({
|
|
46
|
-
children,
|
|
47
|
-
getStreamUrl,
|
|
48
|
-
sessionId,
|
|
49
|
-
}: VideoPlayerProviderProps) {
|
|
50
|
-
const value = useMemo(
|
|
51
|
-
() => ({ getStreamUrl, sessionId }),
|
|
52
|
-
[getStreamUrl, sessionId]
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<VideoPlayerContext.Provider value={value}>
|
|
57
|
-
{children}
|
|
58
|
-
</VideoPlayerContext.Provider>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Hook to access VideoPlayer context
|
|
64
|
-
*/
|
|
65
|
-
export function useVideoPlayerContext(): VideoPlayerContextValue | null {
|
|
66
|
-
return useContext(VideoPlayerContext);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ============================================================================
|
|
70
|
-
// Simplified Stream Source
|
|
71
|
-
// ============================================================================
|
|
72
|
-
|
|
73
|
-
/** Simplified stream source (uses context for getStreamUrl) */
|
|
74
|
-
export interface SimpleStreamSource {
|
|
75
|
-
type: 'stream';
|
|
76
|
-
/** File path on server */
|
|
77
|
-
path: string;
|
|
78
|
-
/** Session ID (optional, uses context if not provided) */
|
|
79
|
-
sessionId?: string;
|
|
80
|
-
/** MIME type for the video */
|
|
81
|
-
mimeType?: string;
|
|
82
|
-
title?: string;
|
|
83
|
-
poster?: string;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Check if source is a simplified stream source (without getStreamUrl)
|
|
88
|
-
*/
|
|
89
|
-
export function isSimpleStreamSource(
|
|
90
|
-
source: VideoSourceUnion | SimpleStreamSource
|
|
91
|
-
): source is SimpleStreamSource {
|
|
92
|
-
return source.type === 'stream' && !('getStreamUrl' in source);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Resolve simplified stream source to full stream source using context
|
|
97
|
-
*/
|
|
98
|
-
export function resolveStreamSource(
|
|
99
|
-
source: SimpleStreamSource,
|
|
100
|
-
context: VideoPlayerContextValue | null
|
|
101
|
-
): StreamSource | null {
|
|
102
|
-
if (!context?.getStreamUrl) {
|
|
103
|
-
console.warn(
|
|
104
|
-
'VideoPlayer: Stream source requires getStreamUrl. ' +
|
|
105
|
-
'Either provide it in source or wrap with VideoPlayerProvider.'
|
|
106
|
-
);
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const sessionId = source.sessionId || context.sessionId;
|
|
111
|
-
if (!sessionId) {
|
|
112
|
-
console.warn('VideoPlayer: Stream source requires sessionId.');
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
type: 'stream',
|
|
118
|
-
sessionId,
|
|
119
|
-
path: source.path,
|
|
120
|
-
getStreamUrl: context.getStreamUrl,
|
|
121
|
-
mimeType: source.mimeType,
|
|
122
|
-
title: source.title,
|
|
123
|
-
poster: source.poster,
|
|
124
|
-
};
|
|
125
|
-
}
|