@stoked-ui/media 0.1.0-alpha.13.1
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/LICENSE +21 -0
- package/README.md +1193 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014 Call-Em-All
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,1193 @@
|
|
|
1
|
+
# @stoked-ui/media
|
|
2
|
+
|
|
3
|
+
A comprehensive media management and component library for React with framework-agnostic abstractions, modern file handling APIs, and reactive media UI components.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `@stoked-ui/media` package provides production-ready media handling capabilities including:
|
|
8
|
+
|
|
9
|
+
- **Media File Management**: Comprehensive file handling with upload, download, and metadata extraction
|
|
10
|
+
- **React Components**: MediaCard and MediaViewer for displaying and managing media
|
|
11
|
+
- **Modern APIs**: File System Access API integration, Zip compression, and streaming support
|
|
12
|
+
- **Framework-Agnostic Abstractions**: Decouple from routing, authentication, payments, and queue systems
|
|
13
|
+
- **API Client**: Type-safe client for consuming the @stoked-ui/media-api backend
|
|
14
|
+
- **React Hooks**: TanStack Query-based hooks for media CRUD operations
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Quick Start](#quick-start)
|
|
20
|
+
- [Core Concepts](#core-concepts)
|
|
21
|
+
- [Components](#components)
|
|
22
|
+
- [MediaCard](#mediacard)
|
|
23
|
+
- [MediaViewer](#mediaviewer)
|
|
24
|
+
- [Media File Management](#media-file-management)
|
|
25
|
+
- [MediaFile Class](#mediafile-class)
|
|
26
|
+
- [WebFile Class](#webfile-class)
|
|
27
|
+
- [File System Access API](#file-system-access-api)
|
|
28
|
+
- [API Client Integration](#api-client-integration)
|
|
29
|
+
- [React Hooks](#react-hooks)
|
|
30
|
+
- [Abstraction Layers](#abstraction-layers)
|
|
31
|
+
- [Advanced Features](#advanced-features)
|
|
32
|
+
- [Metadata Extraction](#metadata-extraction)
|
|
33
|
+
- [Hybrid Metadata Processing](#hybrid-metadata-processing)
|
|
34
|
+
- [Video Sprite Sheet Generation](#video-sprite-sheet-generation)
|
|
35
|
+
- [Performance Optimization](#performance-optimization)
|
|
36
|
+
- [Troubleshooting](#troubleshooting)
|
|
37
|
+
- [FAQ](#faq)
|
|
38
|
+
- [API Reference](#api-reference)
|
|
39
|
+
- [Contributing](#contributing)
|
|
40
|
+
- [License](#license)
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
Install the package using your preferred package manager:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install @stoked-ui/media
|
|
48
|
+
# or
|
|
49
|
+
yarn add @stoked-ui/media
|
|
50
|
+
# or
|
|
51
|
+
pnpm add @stoked-ui/media
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Peer Dependencies
|
|
55
|
+
|
|
56
|
+
The package requires React 18+ and TanStack Query 5+ as peer dependencies:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install react@^18.0.0 @tanstack/react-query@^5.0.0
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Optional Dependencies
|
|
63
|
+
|
|
64
|
+
Some features require additional packages:
|
|
65
|
+
|
|
66
|
+
- **@stoked-ui/common**: Common utilities and components (workspace dependency)
|
|
67
|
+
- **sharp**: For server-side image optimization (optional, for media-api integration)
|
|
68
|
+
- **fluent-ffmpeg**: For video processing (optional, for media-api integration)
|
|
69
|
+
|
|
70
|
+
## Quick Start
|
|
71
|
+
|
|
72
|
+
### 1. Basic Setup with API Client (5 minutes)
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { MediaApiProvider, useMediaList } from '@stoked-ui/media';
|
|
76
|
+
|
|
77
|
+
export function App() {
|
|
78
|
+
return (
|
|
79
|
+
<MediaApiProvider config={{
|
|
80
|
+
baseUrl: 'https://api.example.com',
|
|
81
|
+
}}>
|
|
82
|
+
<MediaGallery />
|
|
83
|
+
</MediaApiProvider>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function MediaGallery() {
|
|
88
|
+
const { data: mediaList, isLoading } = useMediaList();
|
|
89
|
+
|
|
90
|
+
if (isLoading) return <div>Loading...</div>;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
|
|
94
|
+
{mediaList?.items.map((item) => (
|
|
95
|
+
<MediaCard key={item._id} item={item} />
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. MediaCard with Controls
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { MediaCard } from '@stoked-ui/media';
|
|
106
|
+
import { useState } from 'react';
|
|
107
|
+
|
|
108
|
+
export function MyMediaCard() {
|
|
109
|
+
const [modeState, setModeState] = useState({ mode: 'view' });
|
|
110
|
+
|
|
111
|
+
const item = {
|
|
112
|
+
_id: '123',
|
|
113
|
+
title: 'My Video',
|
|
114
|
+
mediaType: 'video',
|
|
115
|
+
file: '/videos/sample.mp4',
|
|
116
|
+
thumbnail: '/thumbnails/sample.jpg',
|
|
117
|
+
duration: 300,
|
|
118
|
+
publicity: 'public',
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<MediaCard
|
|
123
|
+
item={item}
|
|
124
|
+
modeState={modeState}
|
|
125
|
+
setModeState={setModeState}
|
|
126
|
+
info={true}
|
|
127
|
+
onViewClick={(item) => console.log('View:', item._id)}
|
|
128
|
+
onDeleteClick={(item) => console.log('Delete:', item._id)}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 3. MediaViewer with Queue Navigation
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { MediaViewer } from '@stoked-ui/media';
|
|
138
|
+
import { useState } from 'react';
|
|
139
|
+
|
|
140
|
+
export function MyMediaViewer() {
|
|
141
|
+
const [open, setOpen] = useState(false);
|
|
142
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
143
|
+
|
|
144
|
+
const items = [
|
|
145
|
+
{ id: '1', title: 'Video 1', mediaType: 'video' as const, file: '/videos/1.mp4' },
|
|
146
|
+
{ id: '2', title: 'Video 2', mediaType: 'video' as const, file: '/videos/2.mp4' },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<>
|
|
151
|
+
<button onClick={() => setOpen(true)}>Open Viewer</button>
|
|
152
|
+
<MediaViewer
|
|
153
|
+
item={items[currentIndex]}
|
|
154
|
+
mediaItems={items}
|
|
155
|
+
currentIndex={currentIndex}
|
|
156
|
+
open={open}
|
|
157
|
+
onClose={() => setOpen(false)}
|
|
158
|
+
onNavigate={(_, index) => setCurrentIndex(index)}
|
|
159
|
+
enableKeyboardShortcuts
|
|
160
|
+
enableQueue
|
|
161
|
+
/>
|
|
162
|
+
</>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Core Concepts
|
|
168
|
+
|
|
169
|
+
### Media Items
|
|
170
|
+
|
|
171
|
+
Media items are objects that represent images, videos, or albums. They contain metadata like title, description, file URL, thumbnail, and optional server-side metadata.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
interface ExtendedMediaItem {
|
|
175
|
+
_id?: string;
|
|
176
|
+
title?: string;
|
|
177
|
+
description?: string;
|
|
178
|
+
mediaType?: 'image' | 'video' | 'album';
|
|
179
|
+
file?: string;
|
|
180
|
+
url?: string;
|
|
181
|
+
thumbnail?: string;
|
|
182
|
+
duration?: number;
|
|
183
|
+
views?: number;
|
|
184
|
+
publicity?: 'public' | 'private' | 'paid';
|
|
185
|
+
[key: string]: any;
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Server vs. Client Metadata
|
|
190
|
+
|
|
191
|
+
The package supports hybrid metadata processing where metadata can come from:
|
|
192
|
+
|
|
193
|
+
1. **Client-side**: Extracted directly from files using browsers APIs
|
|
194
|
+
2. **Server-side**: Provided by @stoked-ui/media-api for optimized processing
|
|
195
|
+
3. **Hybrid**: Cached and updated dynamically
|
|
196
|
+
|
|
197
|
+
See [Hybrid Metadata Processing](#hybrid-metadata-processing) for details.
|
|
198
|
+
|
|
199
|
+
## Components
|
|
200
|
+
|
|
201
|
+
### MediaCard
|
|
202
|
+
|
|
203
|
+
An interactive card component for displaying media items in galleries or lists with thumbnail previews, progress tracking, and action controls.
|
|
204
|
+
|
|
205
|
+
**Key Features:**
|
|
206
|
+
- Display images and videos with responsive thumbnails
|
|
207
|
+
- Video progress bar with frame-accurate scrubber sprite support
|
|
208
|
+
- Interactive controls (play, edit, delete, toggle public)
|
|
209
|
+
- Selection mode for batch operations and multi-select
|
|
210
|
+
- Payment integration with price display for paid content
|
|
211
|
+
- Queue management with playback tracking
|
|
212
|
+
- Owner vs. viewer mode detection
|
|
213
|
+
- Grid-responsive design with flexible aspect ratios
|
|
214
|
+
- Lazy loading support for performance
|
|
215
|
+
- MediaClass branding support
|
|
216
|
+
|
|
217
|
+
**Complete Props Reference:**
|
|
218
|
+
```typescript
|
|
219
|
+
interface MediaCardProps {
|
|
220
|
+
// Required
|
|
221
|
+
item: ExtendedMediaItem; // Media item to display
|
|
222
|
+
modeState: MediaCardModeState; // Current selection mode state
|
|
223
|
+
setModeState: (state) => void; // Update selection mode
|
|
224
|
+
|
|
225
|
+
// Display options
|
|
226
|
+
info?: boolean; // Show info overlay on hover
|
|
227
|
+
minimalMode?: boolean; // Compact display without metadata
|
|
228
|
+
squareMode?: boolean; // Force 1:1 aspect ratio
|
|
229
|
+
displayMode?: MediaCardDisplayMode; // 'owner' or 'viewer'
|
|
230
|
+
isVisible?: boolean; // For lazy loading optimization
|
|
231
|
+
|
|
232
|
+
// Callbacks
|
|
233
|
+
onViewClick?: (item: ExtendedMediaItem) => void;
|
|
234
|
+
onEditClick?: (item: ExtendedMediaItem) => void;
|
|
235
|
+
onDeleteClick?: (item: ExtendedMediaItem) => void;
|
|
236
|
+
onTogglePublic?: (item: ExtendedMediaItem) => void;
|
|
237
|
+
onThumbnailLoaded?: () => void;
|
|
238
|
+
onMediaLoaded?: () => void;
|
|
239
|
+
|
|
240
|
+
// Abstraction layers (framework integration)
|
|
241
|
+
router?: IRouter;
|
|
242
|
+
auth?: IAuth;
|
|
243
|
+
payment?: IPayment;
|
|
244
|
+
queue?: IQueue;
|
|
245
|
+
|
|
246
|
+
// API integration
|
|
247
|
+
apiClient?: MediaApiClient;
|
|
248
|
+
enableServerThumbnails?: boolean; // Use server-generated thumbnails
|
|
249
|
+
|
|
250
|
+
// Styling
|
|
251
|
+
sx?: SxProps<Theme>; // MUI styling
|
|
252
|
+
imageAlt?: string; // Image alt text
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Usage Examples:**
|
|
257
|
+
```tsx
|
|
258
|
+
// Basic display
|
|
259
|
+
<MediaCard item={media} modeState={modeState} setModeState={setModeState} />
|
|
260
|
+
|
|
261
|
+
// With callbacks
|
|
262
|
+
<MediaCard
|
|
263
|
+
item={media}
|
|
264
|
+
modeState={modeState}
|
|
265
|
+
setModeState={setModeState}
|
|
266
|
+
onViewClick={(item) => navigate(`/media/${item._id}`)}
|
|
267
|
+
onDeleteClick={(item) => deleteMedia(item._id)}
|
|
268
|
+
/>
|
|
269
|
+
|
|
270
|
+
// Owner-specific features
|
|
271
|
+
<MediaCard
|
|
272
|
+
item={media}
|
|
273
|
+
displayMode="owner"
|
|
274
|
+
onEditClick={(item) => navigate(`/edit/${item._id}`)}
|
|
275
|
+
onTogglePublic={(item) => updateMedia(item._id, { publicity: togglePublicity(item.publicity) })}
|
|
276
|
+
/>
|
|
277
|
+
|
|
278
|
+
// With server thumbnails
|
|
279
|
+
<MediaCard
|
|
280
|
+
item={media}
|
|
281
|
+
enableServerThumbnails
|
|
282
|
+
apiClient={apiClient}
|
|
283
|
+
/>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Documentation:** [MediaCard README](./src/components/MediaCard/README.md)
|
|
287
|
+
|
|
288
|
+
### MediaViewer
|
|
289
|
+
|
|
290
|
+
A full-screen media viewer component with multiple view modes, playlist navigation, keyboard shortcuts, and adaptive layout.
|
|
291
|
+
|
|
292
|
+
**Key Features:**
|
|
293
|
+
- Multiple view modes (NORMAL, THEATER, FULLSCREEN)
|
|
294
|
+
- Smooth navigation between media items with prev/next controls
|
|
295
|
+
- Queue integration for playlist management with next-up indicators
|
|
296
|
+
- Keyboard shortcuts for accessibility and power users
|
|
297
|
+
- Responsive layout that adapts to viewport and content
|
|
298
|
+
- Preview grid showing upcoming items
|
|
299
|
+
- MediaClass branding with before/after idents
|
|
300
|
+
- Owner controls for editing and deletion
|
|
301
|
+
- API integration for loading full media details
|
|
302
|
+
- Error handling with fallback display
|
|
303
|
+
|
|
304
|
+
**Complete Props Reference:**
|
|
305
|
+
```typescript
|
|
306
|
+
interface MediaViewerProps {
|
|
307
|
+
// Required
|
|
308
|
+
item: MediaItem; // Current media item
|
|
309
|
+
open: boolean; // Viewer visibility
|
|
310
|
+
onClose: () => void; // Close handler
|
|
311
|
+
|
|
312
|
+
// Navigation
|
|
313
|
+
mediaItems?: MediaItem[]; // Array of all items
|
|
314
|
+
currentIndex?: number; // Current position in array
|
|
315
|
+
onNavigate?: (item: MediaItem, index: number) => void;
|
|
316
|
+
|
|
317
|
+
// Callbacks
|
|
318
|
+
onEdit?: (item: MediaItem) => void;
|
|
319
|
+
onDelete?: (item: MediaItem) => void;
|
|
320
|
+
onMediaLoaded?: (item: MediaItem) => void;
|
|
321
|
+
onMetadataLoaded?: (metadata: Metadata) => void;
|
|
322
|
+
|
|
323
|
+
// Display options
|
|
324
|
+
hideNavbar?: boolean; // Hide toolbar
|
|
325
|
+
showPreviewCards?: boolean; // Show next items preview
|
|
326
|
+
initialMode?: MediaViewerMode; // Starting view mode
|
|
327
|
+
autoplay?: boolean; // Auto-play videos
|
|
328
|
+
initialMuted?: boolean; // Muted on start
|
|
329
|
+
|
|
330
|
+
// Feature toggles
|
|
331
|
+
enableQueue?: boolean; // Enable playlist features
|
|
332
|
+
enableKeyboardShortcuts?: boolean; // Enable keyboard controls
|
|
333
|
+
enableOwnerControls?: boolean; // Show edit/delete buttons
|
|
334
|
+
|
|
335
|
+
// Abstraction layers
|
|
336
|
+
router?: IRouter;
|
|
337
|
+
auth?: IAuth;
|
|
338
|
+
queue?: IQueue;
|
|
339
|
+
keyboard?: IKeyboardShortcuts;
|
|
340
|
+
payment?: IPayment;
|
|
341
|
+
|
|
342
|
+
// API integration
|
|
343
|
+
apiClient?: MediaApiClient;
|
|
344
|
+
enableServerFeatures?: boolean; // Load full details from API
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Usage Examples:**
|
|
349
|
+
```tsx
|
|
350
|
+
// Basic viewer
|
|
351
|
+
<MediaViewer
|
|
352
|
+
item={currentItem}
|
|
353
|
+
open={isOpen}
|
|
354
|
+
onClose={() => setIsOpen(false)}
|
|
355
|
+
/>
|
|
356
|
+
|
|
357
|
+
// With navigation and playlist
|
|
358
|
+
<MediaViewer
|
|
359
|
+
item={items[currentIndex]}
|
|
360
|
+
mediaItems={items}
|
|
361
|
+
currentIndex={currentIndex}
|
|
362
|
+
open={isOpen}
|
|
363
|
+
onClose={() => setIsOpen(false)}
|
|
364
|
+
onNavigate={(item, index) => setCurrentIndex(index)}
|
|
365
|
+
enableQueue
|
|
366
|
+
showPreviewCards
|
|
367
|
+
/>
|
|
368
|
+
|
|
369
|
+
// With all features enabled
|
|
370
|
+
<MediaViewer
|
|
371
|
+
item={currentItem}
|
|
372
|
+
mediaItems={allItems}
|
|
373
|
+
currentIndex={currentIndex}
|
|
374
|
+
open={isOpen}
|
|
375
|
+
onClose={() => setIsOpen(false)}
|
|
376
|
+
onNavigate={(item, index) => setCurrentIndex(index)}
|
|
377
|
+
router={useRouter()}
|
|
378
|
+
auth={useAuth()}
|
|
379
|
+
keyboard={useKeyboard()}
|
|
380
|
+
enableQueue
|
|
381
|
+
enableKeyboardShortcuts
|
|
382
|
+
enableOwnerControls
|
|
383
|
+
apiClient={apiClient}
|
|
384
|
+
/>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
**Keyboard Shortcuts:**
|
|
388
|
+
- `ArrowLeft`: Previous item
|
|
389
|
+
- `ArrowRight`: Next item
|
|
390
|
+
- `f`: Toggle fullscreen
|
|
391
|
+
- `Escape`: Exit fullscreen/close viewer
|
|
392
|
+
- `Space`: Play/pause (video only)
|
|
393
|
+
|
|
394
|
+
**Documentation:** [MediaViewer README](./src/components/MediaViewer/README.md)
|
|
395
|
+
|
|
396
|
+
## Media File Management
|
|
397
|
+
|
|
398
|
+
### MediaFile Class
|
|
399
|
+
|
|
400
|
+
The core `MediaFile` class handles file uploads, downloads, and provides a unified interface for working with files.
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import { MediaFile, type IMediaFile } from '@stoked-ui/media';
|
|
404
|
+
|
|
405
|
+
// Create from File object
|
|
406
|
+
const mediaFile = new MediaFile(fileInput);
|
|
407
|
+
|
|
408
|
+
// Get file metadata
|
|
409
|
+
console.log(mediaFile.name);
|
|
410
|
+
console.log(mediaFile.size);
|
|
411
|
+
console.log(mediaFile.type);
|
|
412
|
+
|
|
413
|
+
// Check media type
|
|
414
|
+
if (mediaFile.isVideo()) {
|
|
415
|
+
console.log('Video file');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Upload to server
|
|
419
|
+
const response = await mediaFile.upload({
|
|
420
|
+
endpoint: '/api/upload',
|
|
421
|
+
headers: { 'Authorization': 'Bearer token' },
|
|
422
|
+
onProgress: (progress) => console.log(`${progress}%`),
|
|
423
|
+
});
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**API Reference:**
|
|
427
|
+
```typescript
|
|
428
|
+
class MediaFile {
|
|
429
|
+
// Properties
|
|
430
|
+
file: File;
|
|
431
|
+
name: string;
|
|
432
|
+
size: number;
|
|
433
|
+
type: string;
|
|
434
|
+
lastModified: number;
|
|
435
|
+
|
|
436
|
+
// Methods
|
|
437
|
+
isImage(): boolean;
|
|
438
|
+
isVideo(): boolean;
|
|
439
|
+
isAudio(): boolean;
|
|
440
|
+
isDocument(): boolean;
|
|
441
|
+
getMediaType(): MediaType;
|
|
442
|
+
|
|
443
|
+
async upload(options: UploadOptions): Promise<UploadResponse>;
|
|
444
|
+
async download(filename?: string): Promise<void>;
|
|
445
|
+
|
|
446
|
+
// File system operations
|
|
447
|
+
async readAsArrayBuffer(): Promise<ArrayBuffer>;
|
|
448
|
+
async readAsDataURL(): Promise<string>;
|
|
449
|
+
async readAsText(): Promise<string>;
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### WebFile Class
|
|
454
|
+
|
|
455
|
+
The `WebFile` class extends `MediaFile` with web-persistent storage capabilities.
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
import { WebFile } from '@stoked-ui/media';
|
|
459
|
+
|
|
460
|
+
// Save file to IndexedDB
|
|
461
|
+
const webFile = new WebFile(fileObject);
|
|
462
|
+
const savedId = await webFile.save();
|
|
463
|
+
|
|
464
|
+
// Retrieve file later
|
|
465
|
+
const retrieved = await WebFile.load(savedId);
|
|
466
|
+
|
|
467
|
+
// List all saved files
|
|
468
|
+
const allFiles = await WebFile.listAll();
|
|
469
|
+
|
|
470
|
+
// Delete file from storage
|
|
471
|
+
await webFile.delete();
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### File System Access API
|
|
475
|
+
|
|
476
|
+
Access the modern File System Access API for native file picker and save dialogs:
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
import { openFileApi, saveFileApi } from '@stoked-ui/media';
|
|
480
|
+
|
|
481
|
+
// Open file picker
|
|
482
|
+
const files = await openFileApi({
|
|
483
|
+
types: [
|
|
484
|
+
{
|
|
485
|
+
description: 'Images',
|
|
486
|
+
accept: { 'image/*': ['.png', '.jpg', '.gif'] }
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
description: 'Videos',
|
|
490
|
+
accept: { 'video/*': ['.mp4', '.webm'] }
|
|
491
|
+
}
|
|
492
|
+
],
|
|
493
|
+
multiple: true
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Save file
|
|
497
|
+
const fileHandle = await saveFileApi({
|
|
498
|
+
suggestedName: 'export.json',
|
|
499
|
+
types: [{
|
|
500
|
+
description: 'JSON',
|
|
501
|
+
accept: { 'application/json': ['.json'] }
|
|
502
|
+
}]
|
|
503
|
+
});
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Browser Support:**
|
|
507
|
+
- Chrome/Edge: Yes
|
|
508
|
+
- Firefox: Partial (behind flag)
|
|
509
|
+
- Safari: Limited support
|
|
510
|
+
- Mobile: Limited support
|
|
511
|
+
|
|
512
|
+
Fallback to standard file input if not available.
|
|
513
|
+
|
|
514
|
+
## API Client Integration
|
|
515
|
+
|
|
516
|
+
### Creating an API Client
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
import { createMediaApiClient } from '@stoked-ui/media';
|
|
520
|
+
|
|
521
|
+
const client = createMediaApiClient({
|
|
522
|
+
baseUrl: 'https://api.example.com/v1',
|
|
523
|
+
authToken: 'your-jwt-token',
|
|
524
|
+
timeout: 30000,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Update token dynamically
|
|
528
|
+
client.setAuthToken(newToken);
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Client Methods
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
// Get single media item
|
|
535
|
+
const media = await client.getMedia(mediaId);
|
|
536
|
+
|
|
537
|
+
// List media with filters
|
|
538
|
+
const list = await client.listMedia({
|
|
539
|
+
limit: 20,
|
|
540
|
+
offset: 0,
|
|
541
|
+
mediaType: 'video',
|
|
542
|
+
search: 'sunset',
|
|
543
|
+
sortBy: '-createdAt',
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Create media entry
|
|
547
|
+
const newMedia = await client.createMedia({
|
|
548
|
+
title: 'My Video',
|
|
549
|
+
description: 'A beautiful sunset',
|
|
550
|
+
mediaType: 'video',
|
|
551
|
+
file: 'https://example.com/sunset.mp4',
|
|
552
|
+
thumbnail: 'https://example.com/sunset.jpg',
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// Update media
|
|
556
|
+
const updated = await client.updateMedia(mediaId, {
|
|
557
|
+
title: 'Updated Title',
|
|
558
|
+
publicity: 'public',
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Delete media
|
|
562
|
+
await client.deleteMedia(mediaId);
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## React Hooks
|
|
566
|
+
|
|
567
|
+
### useMediaApiProvider
|
|
568
|
+
|
|
569
|
+
Set up the API client context:
|
|
570
|
+
|
|
571
|
+
```tsx
|
|
572
|
+
import { MediaApiProvider } from '@stoked-ui/media';
|
|
573
|
+
|
|
574
|
+
export function App() {
|
|
575
|
+
return (
|
|
576
|
+
<MediaApiProvider config={{
|
|
577
|
+
baseUrl: 'https://api.example.com',
|
|
578
|
+
authToken: localStorage.getItem('token'),
|
|
579
|
+
}}>
|
|
580
|
+
<YourApp />
|
|
581
|
+
</MediaApiProvider>
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### useMediaList
|
|
587
|
+
|
|
588
|
+
Fetch paginated media with caching:
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
import { useMediaList } from '@stoked-ui/media';
|
|
592
|
+
|
|
593
|
+
function MediaGallery() {
|
|
594
|
+
const { data, isLoading, error, hasNextPage, fetchNextPage } = useMediaList({
|
|
595
|
+
limit: 20,
|
|
596
|
+
mediaType: 'image',
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
return (
|
|
600
|
+
<div>
|
|
601
|
+
{data?.items.map((item) => (
|
|
602
|
+
<MediaCard key={item._id} item={item} />
|
|
603
|
+
))}
|
|
604
|
+
{hasNextPage && <button onClick={() => fetchNextPage()}>Load More</button>}
|
|
605
|
+
</div>
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### useMediaItem
|
|
611
|
+
|
|
612
|
+
Fetch a single media item with caching:
|
|
613
|
+
|
|
614
|
+
```tsx
|
|
615
|
+
const { data: media, isLoading, error } = useMediaItem(mediaId);
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### useMediaUpload
|
|
619
|
+
|
|
620
|
+
Handle file uploads with progress tracking:
|
|
621
|
+
|
|
622
|
+
```tsx
|
|
623
|
+
const { upload, isUploading, progress, error } = useMediaUpload();
|
|
624
|
+
|
|
625
|
+
const handleUpload = async (file: File) => {
|
|
626
|
+
const result = await upload(file, {
|
|
627
|
+
title: file.name,
|
|
628
|
+
description: 'Auto-uploaded',
|
|
629
|
+
});
|
|
630
|
+
console.log('Uploaded:', result._id);
|
|
631
|
+
};
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### useMediaUpdate
|
|
635
|
+
|
|
636
|
+
Update media metadata:
|
|
637
|
+
|
|
638
|
+
```tsx
|
|
639
|
+
const { update, isUpdating } = useMediaUpdate();
|
|
640
|
+
|
|
641
|
+
const handleUpdate = async (mediaId: string, updates: Partial<MediaItem>) => {
|
|
642
|
+
await update(mediaId, updates);
|
|
643
|
+
};
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### useMediaDelete
|
|
647
|
+
|
|
648
|
+
Delete media:
|
|
649
|
+
|
|
650
|
+
```tsx
|
|
651
|
+
const { delete: deleteMedia, isDeleting } = useMediaDelete();
|
|
652
|
+
|
|
653
|
+
const handleDelete = async (mediaId: string) => {
|
|
654
|
+
await deleteMedia(mediaId);
|
|
655
|
+
};
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
## Abstraction Layers
|
|
659
|
+
|
|
660
|
+
The package uses framework-agnostic abstraction layers to decouple components from specific implementations. This allows components to work with any routing system, auth provider, payment processor, or queue manager.
|
|
661
|
+
|
|
662
|
+
### Available Abstractions
|
|
663
|
+
|
|
664
|
+
1. **Router (`IRouter`)** - Navigation and routing
|
|
665
|
+
2. **Auth (`IAuth`)** - User authentication and authorization
|
|
666
|
+
3. **Payment (`IPayment`)** - Payment processing for paid content
|
|
667
|
+
4. **Queue (`IQueue`)** - Media playback queue management
|
|
668
|
+
5. **KeyboardShortcuts (`IKeyboardShortcuts`)** - Keyboard event handling
|
|
669
|
+
|
|
670
|
+
### Using Abstractions
|
|
671
|
+
|
|
672
|
+
Import the abstractions and create implementations for your framework:
|
|
673
|
+
|
|
674
|
+
```tsx
|
|
675
|
+
import {
|
|
676
|
+
IRouter,
|
|
677
|
+
IAuth,
|
|
678
|
+
IPayment,
|
|
679
|
+
IQueue,
|
|
680
|
+
IKeyboardShortcuts,
|
|
681
|
+
createMockAuth,
|
|
682
|
+
createMockPayment,
|
|
683
|
+
createInMemoryQueue,
|
|
684
|
+
} from '@stoked-ui/media';
|
|
685
|
+
|
|
686
|
+
// Use mock implementations for testing
|
|
687
|
+
const mockAuth = createMockAuth({
|
|
688
|
+
id: 'user-123',
|
|
689
|
+
email: 'user@example.com',
|
|
690
|
+
name: 'John Doe',
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Or create custom implementations
|
|
694
|
+
const customRouter: IRouter = {
|
|
695
|
+
navigate: (path: string) => {
|
|
696
|
+
// Custom navigation logic
|
|
697
|
+
},
|
|
698
|
+
currentPath: () => window.location.pathname,
|
|
699
|
+
query: () => new URLSearchParams(window.location.search),
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// Pass to components
|
|
703
|
+
<MediaCard
|
|
704
|
+
item={mediaItem}
|
|
705
|
+
modeState={modeState}
|
|
706
|
+
setModeState={setModeState}
|
|
707
|
+
router={customRouter}
|
|
708
|
+
auth={mockAuth}
|
|
709
|
+
/>
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### No-Op Implementations
|
|
713
|
+
|
|
714
|
+
For optional abstractions, use no-op implementations:
|
|
715
|
+
|
|
716
|
+
```tsx
|
|
717
|
+
import { noOpRouter, noOpAuth, noOpQueue, noOpPayment, noOpKeyboardShortcuts } from '@stoked-ui/media';
|
|
718
|
+
|
|
719
|
+
<MediaCard
|
|
720
|
+
item={mediaItem}
|
|
721
|
+
modeState={modeState}
|
|
722
|
+
setModeState={setModeState}
|
|
723
|
+
router={noOpRouter}
|
|
724
|
+
auth={noOpAuth}
|
|
725
|
+
/>
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
## TypeScript Support
|
|
729
|
+
|
|
730
|
+
Full TypeScript support with comprehensive type definitions:
|
|
731
|
+
|
|
732
|
+
```tsx
|
|
733
|
+
import type {
|
|
734
|
+
MediaCardProps,
|
|
735
|
+
ExtendedMediaItem,
|
|
736
|
+
MediaCardModeState,
|
|
737
|
+
MediaCardDisplayMode,
|
|
738
|
+
MediaViewerProps,
|
|
739
|
+
MediaItem,
|
|
740
|
+
MediaViewerMode,
|
|
741
|
+
} from '@stoked-ui/media';
|
|
742
|
+
|
|
743
|
+
import { MediaCard, MediaViewer } from '@stoked-ui/media';
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
## Examples
|
|
747
|
+
|
|
748
|
+
### Gallery with Owner Controls
|
|
749
|
+
|
|
750
|
+
```tsx
|
|
751
|
+
import { MediaCard } from '@stoked-ui/media';
|
|
752
|
+
import { useRouter, useAuth } from '@/hooks';
|
|
753
|
+
import { useState } from 'react';
|
|
754
|
+
|
|
755
|
+
export function MediaGallery({ items }) {
|
|
756
|
+
const [modeState, setModeState] = useState({ mode: 'view' });
|
|
757
|
+
const router = useRouter();
|
|
758
|
+
const auth = useAuth();
|
|
759
|
+
|
|
760
|
+
return (
|
|
761
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
|
|
762
|
+
{items.map((item) => (
|
|
763
|
+
<MediaCard
|
|
764
|
+
key={item._id}
|
|
765
|
+
item={item}
|
|
766
|
+
modeState={modeState}
|
|
767
|
+
setModeState={setModeState}
|
|
768
|
+
router={router}
|
|
769
|
+
auth={auth}
|
|
770
|
+
info={true}
|
|
771
|
+
onViewClick={(item) => router.navigate(`/media/${item._id}`)}
|
|
772
|
+
onEditClick={(item) => router.navigate(`/media/${item._id}/edit`)}
|
|
773
|
+
onDeleteClick={async (item) => {
|
|
774
|
+
if (confirm('Delete this media?')) {
|
|
775
|
+
// API call to delete
|
|
776
|
+
}
|
|
777
|
+
}}
|
|
778
|
+
/>
|
|
779
|
+
))}
|
|
780
|
+
</div>
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Media Viewer with Queue
|
|
786
|
+
|
|
787
|
+
```tsx
|
|
788
|
+
import { MediaViewer } from '@stoked-ui/media';
|
|
789
|
+
import { useRouter, useAuth, useQueue, useKeyboardShortcuts } from '@/hooks';
|
|
790
|
+
import { useState } from 'react';
|
|
791
|
+
|
|
792
|
+
export function MediaGalleryViewer({ items, initialIndex }) {
|
|
793
|
+
const [open, setOpen] = useState(false);
|
|
794
|
+
const [currentIndex, setCurrentIndex] = useState(initialIndex || 0);
|
|
795
|
+
|
|
796
|
+
const router = useRouter();
|
|
797
|
+
const auth = useAuth();
|
|
798
|
+
const queue = useQueue();
|
|
799
|
+
const keyboard = useKeyboardShortcuts();
|
|
800
|
+
|
|
801
|
+
const currentItem = items[currentIndex];
|
|
802
|
+
|
|
803
|
+
return (
|
|
804
|
+
<MediaViewer
|
|
805
|
+
item={currentItem}
|
|
806
|
+
mediaItems={items}
|
|
807
|
+
currentIndex={currentIndex}
|
|
808
|
+
open={open}
|
|
809
|
+
onClose={() => setOpen(false)}
|
|
810
|
+
onNavigate={(item, index) => setCurrentIndex(index)}
|
|
811
|
+
router={router}
|
|
812
|
+
auth={auth}
|
|
813
|
+
queue={queue}
|
|
814
|
+
keyboard={keyboard}
|
|
815
|
+
enableQueue={true}
|
|
816
|
+
enableKeyboardShortcuts={true}
|
|
817
|
+
enableOwnerControls={true}
|
|
818
|
+
showPreviewCards={true}
|
|
819
|
+
/>
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
### Integration with Next.js
|
|
825
|
+
|
|
826
|
+
```tsx
|
|
827
|
+
// hooks/useMediaAbstractions.ts
|
|
828
|
+
import { useRouter } from 'next/router';
|
|
829
|
+
import { useSession } from 'next-auth/react';
|
|
830
|
+
import type { IRouter, IAuth } from '@stoked-ui/media';
|
|
831
|
+
|
|
832
|
+
export function useMediaRouter(): IRouter {
|
|
833
|
+
const router = useRouter();
|
|
834
|
+
return {
|
|
835
|
+
navigate: (path: string) => router.push(path),
|
|
836
|
+
currentPath: () => router.pathname,
|
|
837
|
+
query: () => new URLSearchParams(),
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export function useMediaAuth(): IAuth {
|
|
842
|
+
const { data: session } = useSession();
|
|
843
|
+
const user = session?.user;
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
getCurrentUser: () => user ? {
|
|
847
|
+
id: user.id,
|
|
848
|
+
email: user.email,
|
|
849
|
+
name: user.name,
|
|
850
|
+
} : null,
|
|
851
|
+
isAuthenticated: () => !!session,
|
|
852
|
+
login: () => signIn(),
|
|
853
|
+
logout: () => signOut(),
|
|
854
|
+
hasPermission: (resource, action) => true,
|
|
855
|
+
isOwner: (authorId: string) => user?.id === authorId,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Usage
|
|
860
|
+
import { MediaCard } from '@stoked-ui/media';
|
|
861
|
+
import { useMediaRouter, useMediaAuth } from '@/hooks/useMediaAbstractions';
|
|
862
|
+
|
|
863
|
+
export function MyMediaCard({ item }) {
|
|
864
|
+
const [modeState, setModeState] = useState({ mode: 'view' });
|
|
865
|
+
const router = useMediaRouter();
|
|
866
|
+
const auth = useMediaAuth();
|
|
867
|
+
|
|
868
|
+
return (
|
|
869
|
+
<MediaCard
|
|
870
|
+
item={item}
|
|
871
|
+
modeState={modeState}
|
|
872
|
+
setModeState={setModeState}
|
|
873
|
+
router={router}
|
|
874
|
+
auth={auth}
|
|
875
|
+
/>
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
## Advanced Features
|
|
881
|
+
|
|
882
|
+
### Metadata Extraction
|
|
883
|
+
|
|
884
|
+
Extract metadata directly from files in the browser:
|
|
885
|
+
|
|
886
|
+
```tsx
|
|
887
|
+
import { MediaFile } from '@stoked-ui/media';
|
|
888
|
+
|
|
889
|
+
const file = new MediaFile(inputFile);
|
|
890
|
+
|
|
891
|
+
// For videos
|
|
892
|
+
if (file.isVideo()) {
|
|
893
|
+
const video = document.createElement('video');
|
|
894
|
+
video.src = URL.createObjectURL(file.file);
|
|
895
|
+
|
|
896
|
+
video.onloadedmetadata = () => {
|
|
897
|
+
console.log('Duration:', video.duration);
|
|
898
|
+
console.log('Width:', video.videoWidth);
|
|
899
|
+
console.log('Height:', video.videoHeight);
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// For images
|
|
904
|
+
if (file.isImage()) {
|
|
905
|
+
const img = new Image();
|
|
906
|
+
img.src = URL.createObjectURL(file.file);
|
|
907
|
+
|
|
908
|
+
img.onload = () => {
|
|
909
|
+
console.log('Width:', img.width);
|
|
910
|
+
console.log('Height:', img.height);
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
### Hybrid Metadata Processing
|
|
916
|
+
|
|
917
|
+
The package supports hybrid metadata processing where metadata is fetched from both client and server:
|
|
918
|
+
|
|
919
|
+
```tsx
|
|
920
|
+
import { useMediaItem, useMediaMetadataCache } from '@stoked-ui/media';
|
|
921
|
+
|
|
922
|
+
function MediaDisplay({ mediaId }) {
|
|
923
|
+
// Fetch from server (with caching)
|
|
924
|
+
const { data: media } = useMediaItem(mediaId);
|
|
925
|
+
|
|
926
|
+
// Use cached metadata or extract from file
|
|
927
|
+
const metadata = useMediaMetadataCache({
|
|
928
|
+
mediaId,
|
|
929
|
+
serverMetadata: media,
|
|
930
|
+
onMetadataExtracted: (extracted) => {
|
|
931
|
+
console.log('Client-extracted metadata:', extracted);
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
return (
|
|
936
|
+
<div>
|
|
937
|
+
<h2>{media?.title}</h2>
|
|
938
|
+
<p>Duration: {metadata?.duration || media?.duration}</p>
|
|
939
|
+
</div>
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
**Benefits:**
|
|
945
|
+
- **Faster initial load**: Client metadata extracted immediately
|
|
946
|
+
- **Accurate data**: Server metadata used when available
|
|
947
|
+
- **Offline support**: Falls back to client extraction
|
|
948
|
+
- **Progressive enhancement**: Updates as server data loads
|
|
949
|
+
|
|
950
|
+
### Video Sprite Sheet Generation
|
|
951
|
+
|
|
952
|
+
For better scrubbing experience, video thumbnails are generated as sprite sheets:
|
|
953
|
+
|
|
954
|
+
```typescript
|
|
955
|
+
interface SpriteConfig {
|
|
956
|
+
totalFrames: number; // Total frames in sprite
|
|
957
|
+
framesPerRow: number; // Frames per row
|
|
958
|
+
frameWidth: number; // Width of each frame
|
|
959
|
+
frameHeight: number; // Height of each frame
|
|
960
|
+
spriteSheetWidth: number; // Total width
|
|
961
|
+
spriteSheetHeight: number; // Total height
|
|
962
|
+
interval: number; // Seconds between frames
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
The server generates these automatically. Access via:
|
|
967
|
+
|
|
968
|
+
```tsx
|
|
969
|
+
<MediaCard
|
|
970
|
+
item={{
|
|
971
|
+
...media,
|
|
972
|
+
scrubberGenerated: true,
|
|
973
|
+
scrubberSprite: 'https://api.example.com/sprites/123.jpg',
|
|
974
|
+
scrubberSpriteConfig: {
|
|
975
|
+
totalFrames: 100,
|
|
976
|
+
framesPerRow: 10,
|
|
977
|
+
frameWidth: 160,
|
|
978
|
+
frameHeight: 90,
|
|
979
|
+
// ...
|
|
980
|
+
}
|
|
981
|
+
}}
|
|
982
|
+
/>
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
### Performance Optimization
|
|
986
|
+
|
|
987
|
+
**Lazy Loading:**
|
|
988
|
+
```tsx
|
|
989
|
+
const isVisible = useInView(ref);
|
|
990
|
+
|
|
991
|
+
<MediaCard
|
|
992
|
+
item={media}
|
|
993
|
+
isVisible={isVisible}
|
|
994
|
+
// Only loads thumbnail when visible
|
|
995
|
+
/>
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
**Memoization:**
|
|
999
|
+
```tsx
|
|
1000
|
+
import { useMemo } from 'react';
|
|
1001
|
+
import { useMediaViewerLayout } from '@stoked-ui/media';
|
|
1002
|
+
|
|
1003
|
+
const layout = useMediaViewerLayout({
|
|
1004
|
+
item,
|
|
1005
|
+
mediaItems,
|
|
1006
|
+
currentIndex,
|
|
1007
|
+
// Memoized and recalculated only when deps change
|
|
1008
|
+
});
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
**Image Optimization:**
|
|
1012
|
+
- Use server-generated thumbnails when possible
|
|
1013
|
+
- Set appropriate responsive image sizes
|
|
1014
|
+
- Enable AVIF/WebP format support via picture element
|
|
1015
|
+
|
|
1016
|
+
## Troubleshooting
|
|
1017
|
+
|
|
1018
|
+
### Common Issues
|
|
1019
|
+
|
|
1020
|
+
**Issue: "MediaApiProvider not found" error**
|
|
1021
|
+
|
|
1022
|
+
Make sure to wrap your app with `MediaApiProvider`:
|
|
1023
|
+
|
|
1024
|
+
```tsx
|
|
1025
|
+
import { MediaApiProvider } from '@stoked-ui/media';
|
|
1026
|
+
|
|
1027
|
+
export function App() {
|
|
1028
|
+
return (
|
|
1029
|
+
<MediaApiProvider config={{ baseUrl: 'https://api.example.com' }}>
|
|
1030
|
+
<YourApp />
|
|
1031
|
+
</MediaApiProvider>
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
**Issue: Component renders but media doesn't load**
|
|
1037
|
+
|
|
1038
|
+
Check that:
|
|
1039
|
+
1. The media file URL is accessible (CORS headers correct)
|
|
1040
|
+
2. The file format is supported by the browser
|
|
1041
|
+
3. The `mediaType` is set correctly (`'image'` or `'video'`)
|
|
1042
|
+
4. Network request is successful (check DevTools Network tab)
|
|
1043
|
+
|
|
1044
|
+
**Issue: Thumbnails not showing**
|
|
1045
|
+
|
|
1046
|
+
Solutions:
|
|
1047
|
+
1. Ensure thumbnail URL is correct
|
|
1048
|
+
2. Check CORS headers on image server
|
|
1049
|
+
3. Verify image format is supported (JPEG, PNG, WebP)
|
|
1050
|
+
4. Use server-side thumbnail generation for consistency
|
|
1051
|
+
|
|
1052
|
+
**Issue: Video won't play**
|
|
1053
|
+
|
|
1054
|
+
Check:
|
|
1055
|
+
1. Video format is supported (MP4/H.264, WebM/VP9)
|
|
1056
|
+
2. Video codec is compatible
|
|
1057
|
+
3. Server supports range requests for scrubbing
|
|
1058
|
+
4. CORS headers allow video access
|
|
1059
|
+
|
|
1060
|
+
**Issue: Performance degradation with large galleries**
|
|
1061
|
+
|
|
1062
|
+
Solutions:
|
|
1063
|
+
1. Use `isVisible` prop with virtualization
|
|
1064
|
+
2. Implement pagination instead of infinite scroll
|
|
1065
|
+
3. Use server-side thumbnail generation
|
|
1066
|
+
4. Enable browser caching headers
|
|
1067
|
+
|
|
1068
|
+
### Debug Mode
|
|
1069
|
+
|
|
1070
|
+
Enable debug logging:
|
|
1071
|
+
|
|
1072
|
+
```tsx
|
|
1073
|
+
import { MediaApiProvider } from '@stoked-ui/media';
|
|
1074
|
+
|
|
1075
|
+
<MediaApiProvider
|
|
1076
|
+
config={{ baseUrl: 'https://api.example.com' }}
|
|
1077
|
+
debug={true}
|
|
1078
|
+
>
|
|
1079
|
+
<App />
|
|
1080
|
+
</MediaApiProvider>
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
## FAQ
|
|
1084
|
+
|
|
1085
|
+
**Q: Does this work without the @stoked-ui/media-api backend?**
|
|
1086
|
+
|
|
1087
|
+
A: Yes! The components work standalone. Just provide URLs for files and thumbnails. The API client is optional for managing media metadata.
|
|
1088
|
+
|
|
1089
|
+
**Q: Can I use this with a different backend?**
|
|
1090
|
+
|
|
1091
|
+
A: Yes! Implement the `MediaApiClient` interface or use the generic client with your own endpoints.
|
|
1092
|
+
|
|
1093
|
+
**Q: How do I handle authentication?**
|
|
1094
|
+
|
|
1095
|
+
A: Use the `IAuth` abstraction:
|
|
1096
|
+
|
|
1097
|
+
```tsx
|
|
1098
|
+
const auth: IAuth = {
|
|
1099
|
+
getCurrentUser: () => ({ id: 'user-1', email: 'user@example.com' }),
|
|
1100
|
+
isAuthenticated: () => !!localStorage.getItem('token'),
|
|
1101
|
+
login: () => redirectToLogin(),
|
|
1102
|
+
logout: () => clearToken(),
|
|
1103
|
+
hasPermission: (resource, action) => true,
|
|
1104
|
+
isOwner: (authorId) => authorId === currentUserId,
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
<MediaCard item={media} auth={auth} />
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
**Q: How do I implement payment integration?**
|
|
1111
|
+
|
|
1112
|
+
A: Use the `IPayment` abstraction:
|
|
1113
|
+
|
|
1114
|
+
```tsx
|
|
1115
|
+
const payment: IPayment = {
|
|
1116
|
+
initiatePayment: async (options) => {
|
|
1117
|
+
// Call your payment processor (Stripe, PayPal, etc.)
|
|
1118
|
+
return { status: 'completed', transactionId: '...' };
|
|
1119
|
+
},
|
|
1120
|
+
verifyPayment: async (transactionId) => true,
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
<MediaCard item={paidMedia} payment={payment} />
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
**Q: Can I customize the UI?**
|
|
1127
|
+
|
|
1128
|
+
A: Use MUI's `sx` prop for styling:
|
|
1129
|
+
|
|
1130
|
+
```tsx
|
|
1131
|
+
<MediaCard
|
|
1132
|
+
item={media}
|
|
1133
|
+
sx={{
|
|
1134
|
+
'& .media-card': { backgroundColor: '#f5f5f5' },
|
|
1135
|
+
'& .media-title': { fontSize: '1.25rem' },
|
|
1136
|
+
}}
|
|
1137
|
+
/>
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
**Q: How do I implement queue management?**
|
|
1141
|
+
|
|
1142
|
+
A: Use the `IQueue` abstraction:
|
|
1143
|
+
|
|
1144
|
+
```tsx
|
|
1145
|
+
const queue: IQueue = {
|
|
1146
|
+
items: mediaList,
|
|
1147
|
+
add: (item) => { /* add to queue */ },
|
|
1148
|
+
remove: (itemId) => { /* remove from queue */ },
|
|
1149
|
+
clear: () => { /* clear queue */ },
|
|
1150
|
+
next: () => currentItem,
|
|
1151
|
+
previous: () => previousItem,
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
<MediaViewer item={current} queue={queue} enableQueue />
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
## API Reference
|
|
1158
|
+
|
|
1159
|
+
### Type Definitions
|
|
1160
|
+
|
|
1161
|
+
**ExtendedMediaItem** - Represents a media file with metadata
|
|
1162
|
+
|
|
1163
|
+
**MediaCardProps** - Props for MediaCard component
|
|
1164
|
+
|
|
1165
|
+
**MediaViewerProps** - Props for MediaViewer component
|
|
1166
|
+
|
|
1167
|
+
**IRouter** - Abstract router interface
|
|
1168
|
+
|
|
1169
|
+
**IAuth** - Abstract auth interface
|
|
1170
|
+
|
|
1171
|
+
**IPayment** - Abstract payment interface
|
|
1172
|
+
|
|
1173
|
+
**IQueue** - Abstract queue interface
|
|
1174
|
+
|
|
1175
|
+
**IKeyboardShortcuts** - Abstract keyboard shortcuts interface
|
|
1176
|
+
|
|
1177
|
+
See [src/components/MediaCard/README.md](./src/components/MediaCard/README.md) and [src/components/MediaViewer/README.md](./src/components/MediaViewer/README.md) for comprehensive documentation.
|
|
1178
|
+
|
|
1179
|
+
## Storybook
|
|
1180
|
+
|
|
1181
|
+
The package includes comprehensive Storybook stories demonstrating all features. Run:
|
|
1182
|
+
|
|
1183
|
+
```bash
|
|
1184
|
+
npm run storybook
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
## Contributing
|
|
1188
|
+
|
|
1189
|
+
Contributions are welcome! Please follow the [Conventional Commits](https://conventionalcommits.org) specification.
|
|
1190
|
+
|
|
1191
|
+
## License
|
|
1192
|
+
|
|
1193
|
+
MIT - See LICENSE file for details
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stoked-ui/media",
|
|
3
|
+
"author": "Brian Stoker",
|
|
4
|
+
"private": false,
|
|
5
|
+
"version": "0.1.0-alpha.13.1",
|
|
6
|
+
"description": "Comprehensive media management and component library for React with framework-agnostic abstractions, modern file handling APIs, and reactive media UI components",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"react",
|
|
9
|
+
"media",
|
|
10
|
+
"video",
|
|
11
|
+
"image",
|
|
12
|
+
"upload",
|
|
13
|
+
"player",
|
|
14
|
+
"gallery",
|
|
15
|
+
"mui",
|
|
16
|
+
"material-ui",
|
|
17
|
+
"components",
|
|
18
|
+
"file-handling",
|
|
19
|
+
"tanstack-query",
|
|
20
|
+
"api-client",
|
|
21
|
+
"metadata",
|
|
22
|
+
"thumbnails"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"main": "./node/index.js",
|
|
26
|
+
"files": [
|
|
27
|
+
"build"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/stoked-ui/sui",
|
|
32
|
+
"directory": "packages/sui-media"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"directory": "build"
|
|
37
|
+
},
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@stoked-ui/common": "0.1.3-a.0",
|
|
41
|
+
"@tanstack/react-query": "^5.0.0",
|
|
42
|
+
"react": "^18.0.0",
|
|
43
|
+
"react-dom": "^18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@emotion/react": "^11.11.4",
|
|
47
|
+
"@emotion/styled": "^11.11.5",
|
|
48
|
+
"@mui/icons-material": "^5.15.20",
|
|
49
|
+
"@mui/lab": "^5.0.0-alpha.170",
|
|
50
|
+
"@mui/material": "^5.15.20",
|
|
51
|
+
"formdata-node": "^6.0.3",
|
|
52
|
+
"jszip": "^3.10.1",
|
|
53
|
+
"@tanstack/react-query": "^5.0.0"
|
|
54
|
+
},
|
|
55
|
+
"module": "./index.js",
|
|
56
|
+
"types": "./index.d.ts"
|
|
57
|
+
}
|