@djangocfg/ui-tools 2.1.402 → 2.1.407
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/README.md +16 -1
- package/package.json +11 -9
- package/src/tools/AudioPlayer/lazy.tsx +13 -27
- package/src/tools/ImageViewer/components/ImageViewer.tsx +10 -2
- package/src/tools/JsonForm/JsonSchemaForm.tsx +3 -1
- package/src/tools/JsonForm/README.md +12 -2
- package/src/tools/JsonForm/widgets/TextareaWidget.tsx +25 -0
- package/src/tools/JsonForm/widgets/index.ts +1 -0
- package/src/tools/JsonTree/README.md +12 -0
- package/src/tools/PrettyCode/README.md +81 -0
- package/src/tools/VideoPlayer/README.md +87 -230
- package/src/tools/VideoPlayer/VideoPlayer.tsx +82 -0
- package/src/tools/VideoPlayer/canvas/canvas-dispatcher.tsx +34 -0
- package/src/tools/VideoPlayer/canvas/hls-canvas.tsx +38 -0
- package/src/tools/VideoPlayer/canvas/iframe-canvas.tsx +33 -0
- package/src/tools/VideoPlayer/canvas/index.ts +12 -0
- package/src/tools/VideoPlayer/canvas/jsx.d.ts +54 -0
- package/src/tools/VideoPlayer/canvas/native-canvas.tsx +38 -0
- package/src/tools/VideoPlayer/canvas/vimeo-canvas.tsx +39 -0
- package/src/tools/VideoPlayer/canvas/youtube-canvas.tsx +77 -0
- package/src/tools/VideoPlayer/index.ts +51 -65
- package/src/tools/VideoPlayer/lazy.tsx +11 -54
- package/src/tools/VideoPlayer/parts/controls-bar.tsx +35 -0
- package/src/tools/VideoPlayer/parts/fullscreen.tsx +19 -0
- package/src/tools/VideoPlayer/parts/index.ts +15 -0
- package/src/tools/VideoPlayer/parts/pip.tsx +19 -0
- package/src/tools/VideoPlayer/parts/play-button.tsx +19 -0
- package/src/tools/VideoPlayer/parts/playback-rate.tsx +31 -0
- package/src/tools/VideoPlayer/parts/poster.tsx +3 -0
- package/src/tools/VideoPlayer/parts/seek-bar.tsx +26 -0
- package/src/tools/VideoPlayer/parts/volume.tsx +32 -0
- package/src/tools/VideoPlayer/styles/video-player.css +141 -0
- package/src/tools/VideoPlayer/types.ts +82 -0
- package/src/tools/VideoPlayer/utils/parse-embed-url.ts +70 -0
- package/src/tools/VideoPlayer/utils/vimeo-id.ts +24 -0
- package/src/tools/VideoPlayer/utils/youtube-id.ts +64 -0
- package/src/tools/index.ts +35 -28
- package/src/tools/VideoPlayer/components/VideoControls.tsx +0 -138
- package/src/tools/VideoPlayer/components/VideoErrorFallback.tsx +0 -172
- package/src/tools/VideoPlayer/components/VideoPlayer.tsx +0 -201
- package/src/tools/VideoPlayer/components/index.ts +0 -14
- package/src/tools/VideoPlayer/context/VideoPlayerContext.tsx +0 -52
- package/src/tools/VideoPlayer/context/index.ts +0 -8
- package/src/tools/VideoPlayer/hooks/index.ts +0 -12
- package/src/tools/VideoPlayer/hooks/useVideoPlayerSettings.ts +0 -71
- package/src/tools/VideoPlayer/hooks/useVideoPositionCache.ts +0 -117
- package/src/tools/VideoPlayer/providers/NativeProvider.tsx +0 -284
- package/src/tools/VideoPlayer/providers/StreamProvider.tsx +0 -505
- package/src/tools/VideoPlayer/providers/VidstackProvider.tsx +0 -397
- package/src/tools/VideoPlayer/providers/index.ts +0 -8
- package/src/tools/VideoPlayer/types/index.ts +0 -38
- package/src/tools/VideoPlayer/types/player.ts +0 -116
- package/src/tools/VideoPlayer/types/provider.ts +0 -93
- package/src/tools/VideoPlayer/types/sources.ts +0 -97
- package/src/tools/VideoPlayer/utils/debug.ts +0 -14
- package/src/tools/VideoPlayer/utils/fileSource.ts +0 -78
- package/src/tools/VideoPlayer/utils/index.ts +0 -12
- package/src/tools/VideoPlayer/utils/resolvers.ts +0 -75
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VideoPlayer types — discriminated `VideoSource` union + props.
|
|
3
|
+
*
|
|
4
|
+
* The component is engine-agnostic at the API surface: callers either
|
|
5
|
+
* pass a structured `VideoSource` or a raw URL string (auto-parsed via
|
|
6
|
+
* `parseEmbedUrl`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
|
|
11
|
+
export interface UrlSource {
|
|
12
|
+
readonly type: 'url';
|
|
13
|
+
readonly url: string;
|
|
14
|
+
readonly mimeType?: string;
|
|
15
|
+
readonly title?: string;
|
|
16
|
+
readonly poster?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface YouTubeSource {
|
|
20
|
+
readonly type: 'youtube';
|
|
21
|
+
readonly videoId: string;
|
|
22
|
+
readonly startTime?: number;
|
|
23
|
+
readonly playlistId?: string;
|
|
24
|
+
readonly title?: string;
|
|
25
|
+
readonly poster?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface VimeoSource {
|
|
29
|
+
readonly type: 'vimeo';
|
|
30
|
+
readonly videoId: string;
|
|
31
|
+
readonly startTime?: number;
|
|
32
|
+
readonly title?: string;
|
|
33
|
+
readonly poster?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HlsSource {
|
|
37
|
+
readonly type: 'hls';
|
|
38
|
+
readonly url: string;
|
|
39
|
+
readonly title?: string;
|
|
40
|
+
readonly poster?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface IframeSource {
|
|
44
|
+
readonly type: 'iframe';
|
|
45
|
+
readonly url: string;
|
|
46
|
+
readonly title?: string;
|
|
47
|
+
readonly poster?: string;
|
|
48
|
+
readonly allow?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type VideoSource =
|
|
52
|
+
| UrlSource
|
|
53
|
+
| YouTubeSource
|
|
54
|
+
| VimeoSource
|
|
55
|
+
| HlsSource
|
|
56
|
+
| IframeSource;
|
|
57
|
+
|
|
58
|
+
export type AspectRatioValue = number | 'auto' | 'fill';
|
|
59
|
+
|
|
60
|
+
export interface VideoPlayerSettings {
|
|
61
|
+
readonly autoPlay?: boolean;
|
|
62
|
+
readonly muted?: boolean;
|
|
63
|
+
readonly loop?: boolean;
|
|
64
|
+
readonly playsInline?: boolean;
|
|
65
|
+
readonly crossOrigin?: '' | 'anonymous' | 'use-credentials';
|
|
66
|
+
readonly preload?: 'none' | 'metadata' | 'auto';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface VideoPlayerProps extends VideoPlayerSettings {
|
|
70
|
+
/**
|
|
71
|
+
* Structured source object — or a raw URL string that will be
|
|
72
|
+
* auto-classified via `parseEmbedUrl` (YouTube / Vimeo / HLS / MP4 / iframe).
|
|
73
|
+
*/
|
|
74
|
+
readonly source: VideoSource | string;
|
|
75
|
+
/** Default `true`. When `false`, no built-in `<MediaControlBar>` is rendered. */
|
|
76
|
+
readonly controls?: boolean;
|
|
77
|
+
/** Default `16/9`. `'fill'` stretches to parent height; `'auto'` keeps intrinsic. */
|
|
78
|
+
readonly aspectRatio?: AspectRatioValue;
|
|
79
|
+
readonly className?: string;
|
|
80
|
+
/** Custom children replace the default control bar entirely. */
|
|
81
|
+
readonly children?: ReactNode;
|
|
82
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart URL classifier — maps an arbitrary string into a structured
|
|
3
|
+
* `VideoSource`.
|
|
4
|
+
*
|
|
5
|
+
* Order:
|
|
6
|
+
* 1. YouTube (youtube.com, youtu.be, shorts, embed)
|
|
7
|
+
* 2. Vimeo (vimeo.com, player.vimeo.com)
|
|
8
|
+
* 3. HLS (`.m3u8`)
|
|
9
|
+
* 4. Native (`.mp4`, `.webm`, `.mov`, `.mkv`, `.ogv`, `.m4v`)
|
|
10
|
+
* 5. Fallback — treat as iframe embed.
|
|
11
|
+
*
|
|
12
|
+
* Pure; no React, no DOM.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { VideoSource } from '../types';
|
|
16
|
+
import { extractYouTubeId, parseYouTubeStartTime } from './youtube-id';
|
|
17
|
+
import { extractVimeoId } from './vimeo-id';
|
|
18
|
+
|
|
19
|
+
const NATIVE_VIDEO_EXT = /\.(mp4|webm|mov|mkv|ogv|m4v)(\?.*)?$/i;
|
|
20
|
+
const HLS_EXT = /\.m3u8(\?.*)?$/i;
|
|
21
|
+
|
|
22
|
+
export function parseEmbedUrl(input: string): VideoSource {
|
|
23
|
+
const trimmed = input.trim();
|
|
24
|
+
|
|
25
|
+
// 1. YouTube.
|
|
26
|
+
const ytId = extractYouTubeId(trimmed);
|
|
27
|
+
if (ytId) {
|
|
28
|
+
let startTime: number | undefined;
|
|
29
|
+
let playlistId: string | undefined;
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(trimmed);
|
|
32
|
+
startTime = parseYouTubeStartTime(url.searchParams.get('t'));
|
|
33
|
+
const list = url.searchParams.get('list');
|
|
34
|
+
if (list && /^[\w-]+$/.test(list)) playlistId = list;
|
|
35
|
+
} catch {
|
|
36
|
+
/* ignore */
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
type: 'youtube',
|
|
40
|
+
videoId: ytId,
|
|
41
|
+
...(startTime !== undefined && { startTime }),
|
|
42
|
+
...(playlistId && { playlistId }),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Vimeo.
|
|
47
|
+
const vimeoId = extractVimeoId(trimmed);
|
|
48
|
+
if (vimeoId) {
|
|
49
|
+
return { type: 'vimeo', videoId: vimeoId };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 3. HLS.
|
|
53
|
+
if (HLS_EXT.test(trimmed)) {
|
|
54
|
+
return { type: 'hls', url: trimmed };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 4. Native video file.
|
|
58
|
+
if (NATIVE_VIDEO_EXT.test(trimmed)) {
|
|
59
|
+
return { type: 'url', url: trimmed };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 5. Fallback: blob: / data: / unknown http(s) URL — assume native <video>
|
|
63
|
+
// can handle blob/data, iframe for everything else.
|
|
64
|
+
if (/^(blob:|data:)/i.test(trimmed)) {
|
|
65
|
+
return { type: 'url', url: trimmed };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Unknown URL — fall back to iframe so embeds still surface.
|
|
69
|
+
return { type: 'iframe', url: trimmed };
|
|
70
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a Vimeo numeric `videoId` from a URL.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - vimeo.com/123456789
|
|
6
|
+
* - vimeo.com/123456789/HASH
|
|
7
|
+
* - player.vimeo.com/video/123456789
|
|
8
|
+
*
|
|
9
|
+
* Returns `null` if the URL does not match.
|
|
10
|
+
*/
|
|
11
|
+
export function extractVimeoId(input: string): string | null {
|
|
12
|
+
let url: URL;
|
|
13
|
+
try {
|
|
14
|
+
url = new URL(input);
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const host = url.hostname.replace(/^www\./, '');
|
|
19
|
+
if (host !== 'vimeo.com' && host !== 'player.vimeo.com') return null;
|
|
20
|
+
|
|
21
|
+
// /video/<id> on player.vimeo.com, /<id> on vimeo.com.
|
|
22
|
+
const match = url.pathname.match(/(?:^\/video\/|^\/)(\d+)/);
|
|
23
|
+
return match ? match[1] : null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a YouTube `videoId` from a URL.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - youtube.com/watch?v=ID
|
|
6
|
+
* - youtu.be/ID
|
|
7
|
+
* - youtube.com/shorts/ID
|
|
8
|
+
* - youtube.com/embed/ID
|
|
9
|
+
* - youtube.com/v/ID
|
|
10
|
+
* - m.youtube.com/* mirrors
|
|
11
|
+
*
|
|
12
|
+
* Returns `null` if the URL does not match.
|
|
13
|
+
*/
|
|
14
|
+
export function extractYouTubeId(input: string): string | null {
|
|
15
|
+
let url: URL;
|
|
16
|
+
try {
|
|
17
|
+
url = new URL(input);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const host = url.hostname.replace(/^www\.|^m\./, '');
|
|
23
|
+
|
|
24
|
+
if (host === 'youtu.be') {
|
|
25
|
+
const id = url.pathname.replace(/^\//, '').split('/')[0];
|
|
26
|
+
return isValidId(id) ? id : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (host === 'youtube.com' || host === 'youtube-nocookie.com') {
|
|
30
|
+
if (url.pathname === '/watch') {
|
|
31
|
+
const v = url.searchParams.get('v');
|
|
32
|
+
return v && isValidId(v) ? v : null;
|
|
33
|
+
}
|
|
34
|
+
const m = url.pathname.match(/^\/(?:shorts|embed|v|live)\/([^/?#]+)/);
|
|
35
|
+
if (m) {
|
|
36
|
+
return isValidId(m[1]) ? m[1] : null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Parse the YouTube `?t=` / `&t=` start-time param (`90`, `90s`, `1m30s`). */
|
|
44
|
+
export function parseYouTubeStartTime(input: string | null): number | undefined {
|
|
45
|
+
if (!input) return undefined;
|
|
46
|
+
// Pure seconds.
|
|
47
|
+
if (/^\d+s?$/.test(input)) {
|
|
48
|
+
const n = parseInt(input, 10);
|
|
49
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
50
|
+
}
|
|
51
|
+
// `1h2m3s` / `2m30s` / `45s`.
|
|
52
|
+
const match = input.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/);
|
|
53
|
+
if (!match) return undefined;
|
|
54
|
+
const [, h, m, s] = match;
|
|
55
|
+
const total =
|
|
56
|
+
(h ? parseInt(h, 10) * 3600 : 0) +
|
|
57
|
+
(m ? parseInt(m, 10) * 60 : 0) +
|
|
58
|
+
(s ? parseInt(s, 10) : 0);
|
|
59
|
+
return total > 0 ? total : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isValidId(id: string | undefined): id is string {
|
|
63
|
+
return !!id && /^[\w-]{6,}$/.test(id);
|
|
64
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -43,43 +43,50 @@ export * from './JsonForm/utils';
|
|
|
43
43
|
export { default as OpenapiViewer } from './OpenapiViewer';
|
|
44
44
|
export type { PlaygroundConfig, SchemaSource, PlaygroundProps } from './OpenapiViewer';
|
|
45
45
|
|
|
46
|
-
// Export VideoPlayer
|
|
46
|
+
// Export VideoPlayer (media-chrome shell with provider-aware canvases)
|
|
47
47
|
export {
|
|
48
48
|
VideoPlayer,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
CanvasDispatcher,
|
|
50
|
+
NativeCanvas,
|
|
51
|
+
YouTubeCanvas,
|
|
52
|
+
VimeoCanvas,
|
|
53
|
+
HlsCanvas,
|
|
54
|
+
IframeCanvas,
|
|
55
|
+
PlayButton,
|
|
56
|
+
SeekBar,
|
|
57
|
+
Volume,
|
|
58
|
+
Fullscreen,
|
|
59
|
+
Pip,
|
|
60
|
+
PlaybackRate,
|
|
61
|
+
ControlsBar,
|
|
62
|
+
Poster,
|
|
63
|
+
parseEmbedUrl,
|
|
64
|
+
extractYouTubeId,
|
|
65
|
+
extractVimeoId,
|
|
61
66
|
} from './VideoPlayer';
|
|
62
67
|
export type {
|
|
63
|
-
|
|
68
|
+
VideoSource,
|
|
64
69
|
UrlSource,
|
|
65
70
|
YouTubeSource,
|
|
66
71
|
VimeoSource,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
StreamSource,
|
|
70
|
-
BlobSource,
|
|
71
|
-
DataUrlSource,
|
|
72
|
-
PlayerMode,
|
|
72
|
+
HlsSource,
|
|
73
|
+
IframeSource,
|
|
73
74
|
AspectRatioValue,
|
|
74
75
|
VideoPlayerProps,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
VideoPlayerSettings,
|
|
77
|
+
PlayButtonProps,
|
|
78
|
+
SeekBarProps,
|
|
79
|
+
VolumeProps,
|
|
80
|
+
FullscreenProps,
|
|
81
|
+
PipProps,
|
|
82
|
+
PlaybackRateProps,
|
|
83
|
+
ControlsBarProps,
|
|
84
|
+
CanvasDispatcherProps,
|
|
85
|
+
NativeCanvasProps,
|
|
86
|
+
YouTubeCanvasProps,
|
|
87
|
+
VimeoCanvasProps,
|
|
88
|
+
HlsCanvasProps,
|
|
89
|
+
IframeCanvasProps,
|
|
83
90
|
} from './VideoPlayer';
|
|
84
91
|
|
|
85
92
|
// AudioPlayer v6 — under construction (see @dev/@refactoring6-audioplayer/).
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Custom Video Controls for Vidstack Player
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
'use client';
|
|
6
|
-
|
|
7
|
-
import { Maximize, Minimize, Pause, Play, Volume2, VolumeX } from 'lucide-react';
|
|
8
|
-
import React from 'react';
|
|
9
|
-
|
|
10
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
11
|
-
import { useMediaRemote, useMediaStore } from '@vidstack/react';
|
|
12
|
-
|
|
13
|
-
import type { MediaPlayerInstance } from '@vidstack/react';
|
|
14
|
-
interface VideoControlsProps {
|
|
15
|
-
player: React.RefObject<MediaPlayerInstance | null>;
|
|
16
|
-
className?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function VideoControls({ player, className }: VideoControlsProps) {
|
|
20
|
-
const store = useMediaStore(player);
|
|
21
|
-
const remote = useMediaRemote();
|
|
22
|
-
|
|
23
|
-
const isPaused = store.paused;
|
|
24
|
-
const isMuted = store.muted;
|
|
25
|
-
const isFullscreen = store.fullscreen;
|
|
26
|
-
const currentTime = store.currentTime;
|
|
27
|
-
const duration = store.duration;
|
|
28
|
-
const volume = store.volume;
|
|
29
|
-
|
|
30
|
-
const formatTime = (seconds: number): string => {
|
|
31
|
-
if (!seconds || seconds < 0) return '0:00';
|
|
32
|
-
const minutes = Math.floor(seconds / 60);
|
|
33
|
-
const secs = Math.floor(seconds % 60);
|
|
34
|
-
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
38
|
-
if (!duration) return;
|
|
39
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
40
|
-
const clickX = e.clientX - rect.left;
|
|
41
|
-
const percentage = clickX / rect.width;
|
|
42
|
-
const newTime = percentage * duration;
|
|
43
|
-
remote.seek(newTime);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const handleVolumeChange = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
47
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
48
|
-
const clickX = e.clientX - rect.left;
|
|
49
|
-
const percentage = Math.max(0, Math.min(1, clickX / rect.width));
|
|
50
|
-
remote.changeVolume(percentage);
|
|
51
|
-
if (percentage > 0 && isMuted) {
|
|
52
|
-
remote.toggleMuted();
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div
|
|
60
|
-
className={cn(
|
|
61
|
-
"absolute inset-0 flex flex-col justify-end transition-opacity duration-300",
|
|
62
|
-
"bg-gradient-to-t from-black/80 via-black/20 to-transparent",
|
|
63
|
-
"opacity-0 group-hover:opacity-100 focus-within:opacity-100",
|
|
64
|
-
"pointer-events-none group-hover:pointer-events-auto",
|
|
65
|
-
className
|
|
66
|
-
)}
|
|
67
|
-
>
|
|
68
|
-
{/* Progress Bar */}
|
|
69
|
-
<div className="px-4 pb-2 pointer-events-auto">
|
|
70
|
-
<div
|
|
71
|
-
className="h-1.5 bg-white/20 rounded-full cursor-pointer hover:h-2 transition-all group"
|
|
72
|
-
onClick={handleProgressClick}
|
|
73
|
-
>
|
|
74
|
-
<div
|
|
75
|
-
className="h-full bg-primary rounded-full transition-all relative group-hover:bg-primary/90"
|
|
76
|
-
style={{ width: `${progress}%` }}
|
|
77
|
-
>
|
|
78
|
-
<div className="absolute right-0 top-1/2 -translate-y-1/2 w-3 h-3 bg-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
|
-
{/* Control Bar */}
|
|
84
|
-
<div className="flex items-center gap-4 px-4 pb-4 pointer-events-auto">
|
|
85
|
-
{/* Play/Pause */}
|
|
86
|
-
<button
|
|
87
|
-
onClick={() => remote.togglePaused()}
|
|
88
|
-
className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
|
|
89
|
-
>
|
|
90
|
-
{isPaused ? <Play className="h-6 w-6" /> : <Pause className="h-6 w-6" />}
|
|
91
|
-
</button>
|
|
92
|
-
|
|
93
|
-
{/* Time */}
|
|
94
|
-
<div className="text-white text-sm font-medium">
|
|
95
|
-
{formatTime(currentTime)} / {formatTime(duration)}
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
<div className="flex-1" />
|
|
99
|
-
|
|
100
|
-
{/* Volume Control */}
|
|
101
|
-
<div className="flex items-center gap-2 group/volume">
|
|
102
|
-
<button
|
|
103
|
-
onClick={() => remote.toggleMuted()}
|
|
104
|
-
className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
|
|
105
|
-
>
|
|
106
|
-
{isMuted || volume === 0 ? (
|
|
107
|
-
<VolumeX className="h-5 w-5" />
|
|
108
|
-
) : (
|
|
109
|
-
<Volume2 className="h-5 w-5" />
|
|
110
|
-
)}
|
|
111
|
-
</button>
|
|
112
|
-
|
|
113
|
-
<div
|
|
114
|
-
className="w-0 group-hover/volume:w-20 transition-all overflow-hidden"
|
|
115
|
-
>
|
|
116
|
-
<div
|
|
117
|
-
className="h-1.5 bg-white/20 rounded-full cursor-pointer hover:h-2 transition-all"
|
|
118
|
-
onClick={handleVolumeChange}
|
|
119
|
-
>
|
|
120
|
-
<div
|
|
121
|
-
className="h-full bg-white rounded-full transition-all"
|
|
122
|
-
style={{ width: `${volume * 100}%` }}
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
{/* Fullscreen */}
|
|
129
|
-
<button
|
|
130
|
-
onClick={() => isFullscreen ? remote.exitFullscreen() : remote.enterFullscreen()}
|
|
131
|
-
className="text-white hover:text-primary transition-colors p-1.5 hover:bg-white/10 rounded-full"
|
|
132
|
-
>
|
|
133
|
-
{isFullscreen ? <Minimize className="h-5 w-5" /> : <Maximize className="h-5 w-5" />}
|
|
134
|
-
</button>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VideoErrorFallback - Pre-built error fallback with download button
|
|
3
|
-
* For use with VideoPlayer errorFallback prop
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
'use client';
|
|
7
|
-
|
|
8
|
-
import React from 'react';
|
|
9
|
-
import { FileVideo, RefreshCw } from 'lucide-react';
|
|
10
|
-
|
|
11
|
-
import { cn, Button, DownloadButton } from '../../_shared';
|
|
12
|
-
|
|
13
|
-
import type { ErrorFallbackProps } from '../types';
|
|
14
|
-
|
|
15
|
-
// ============================================================================
|
|
16
|
-
// Types
|
|
17
|
-
// ============================================================================
|
|
18
|
-
|
|
19
|
-
export interface VideoErrorFallbackProps extends ErrorFallbackProps {
|
|
20
|
-
/** URL for download button (if provided, shows download button) */
|
|
21
|
-
downloadUrl?: string;
|
|
22
|
-
/** Filename for download */
|
|
23
|
-
downloadFilename?: string;
|
|
24
|
-
/** File size to display */
|
|
25
|
-
fileSize?: string;
|
|
26
|
-
/** Show retry button */
|
|
27
|
-
showRetry?: boolean;
|
|
28
|
-
/** Custom className */
|
|
29
|
-
className?: string;
|
|
30
|
-
/** Custom icon */
|
|
31
|
-
icon?: React.ReactNode;
|
|
32
|
-
/** Custom title (defaults to error message) */
|
|
33
|
-
title?: string;
|
|
34
|
-
/** Custom description */
|
|
35
|
-
description?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ============================================================================
|
|
39
|
-
// Component
|
|
40
|
-
// ============================================================================
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Pre-built error fallback component for VideoPlayer
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* // Basic usage
|
|
47
|
-
* <VideoPlayer
|
|
48
|
-
* source={source}
|
|
49
|
-
* errorFallback={(props) => (
|
|
50
|
-
* <VideoErrorFallback
|
|
51
|
-
* {...props}
|
|
52
|
-
* downloadUrl={getDownloadUrl()}
|
|
53
|
-
* downloadFilename="video.mp4"
|
|
54
|
-
* />
|
|
55
|
-
* )}
|
|
56
|
-
* />
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* // With file size
|
|
60
|
-
* <VideoErrorFallback
|
|
61
|
-
* error="Failed to load video"
|
|
62
|
-
* downloadUrl="/api/download/video.mp4"
|
|
63
|
-
* fileSize="125 MB"
|
|
64
|
-
* showRetry
|
|
65
|
-
* retry={() => reloadVideo()}
|
|
66
|
-
* />
|
|
67
|
-
*/
|
|
68
|
-
export function VideoErrorFallback({
|
|
69
|
-
error,
|
|
70
|
-
retry,
|
|
71
|
-
downloadUrl,
|
|
72
|
-
downloadFilename,
|
|
73
|
-
fileSize,
|
|
74
|
-
showRetry = true,
|
|
75
|
-
className,
|
|
76
|
-
icon,
|
|
77
|
-
title,
|
|
78
|
-
description,
|
|
79
|
-
}: VideoErrorFallbackProps) {
|
|
80
|
-
const displayTitle = title || error || 'Video cannot be previewed';
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<div
|
|
84
|
-
className={cn(
|
|
85
|
-
'absolute inset-0 flex flex-col items-center justify-center gap-4 bg-black/90 text-white p-6',
|
|
86
|
-
className
|
|
87
|
-
)}
|
|
88
|
-
>
|
|
89
|
-
{/* Icon */}
|
|
90
|
-
{icon || <FileVideo className="w-16 h-16 text-muted-foreground" />}
|
|
91
|
-
|
|
92
|
-
{/* Title */}
|
|
93
|
-
<p className="text-lg font-medium text-center">{displayTitle}</p>
|
|
94
|
-
|
|
95
|
-
{/* Description / File size */}
|
|
96
|
-
{(description || fileSize) && (
|
|
97
|
-
<p className="text-sm text-muted-foreground text-center">
|
|
98
|
-
{description || fileSize}
|
|
99
|
-
</p>
|
|
100
|
-
)}
|
|
101
|
-
|
|
102
|
-
{/* Actions */}
|
|
103
|
-
<div className="flex items-center gap-3 mt-2">
|
|
104
|
-
{/* Retry button */}
|
|
105
|
-
{showRetry && retry && (
|
|
106
|
-
<Button
|
|
107
|
-
variant="outline"
|
|
108
|
-
size="sm"
|
|
109
|
-
onClick={retry}
|
|
110
|
-
className="gap-2"
|
|
111
|
-
>
|
|
112
|
-
<RefreshCw className="w-4 h-4" />
|
|
113
|
-
Retry
|
|
114
|
-
</Button>
|
|
115
|
-
)}
|
|
116
|
-
|
|
117
|
-
{/* Download button */}
|
|
118
|
-
{downloadUrl && (
|
|
119
|
-
<DownloadButton
|
|
120
|
-
url={downloadUrl}
|
|
121
|
-
filename={downloadFilename}
|
|
122
|
-
variant="default"
|
|
123
|
-
size="sm"
|
|
124
|
-
>
|
|
125
|
-
Download to view
|
|
126
|
-
</DownloadButton>
|
|
127
|
-
)}
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ============================================================================
|
|
134
|
-
// Factory for common use cases
|
|
135
|
-
// ============================================================================
|
|
136
|
-
|
|
137
|
-
export interface CreateVideoErrorFallbackOptions {
|
|
138
|
-
/** Function to get download URL from source */
|
|
139
|
-
getDownloadUrl?: (source: unknown) => string | undefined;
|
|
140
|
-
/** Function to get filename from source */
|
|
141
|
-
getFilename?: (source: unknown) => string | undefined;
|
|
142
|
-
/** Function to get file size from source */
|
|
143
|
-
getFileSize?: (source: unknown) => string | undefined;
|
|
144
|
-
/** Show retry button */
|
|
145
|
-
showRetry?: boolean;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Factory to create error fallback function for VideoPlayer
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* const errorFallback = createVideoErrorFallback({
|
|
153
|
-
* getDownloadUrl: (source) => source.downloadUrl,
|
|
154
|
-
* getFilename: (source) => source.filename,
|
|
155
|
-
* showRetry: true,
|
|
156
|
-
* });
|
|
157
|
-
*
|
|
158
|
-
* <VideoPlayer source={source} errorFallback={errorFallback} />
|
|
159
|
-
*/
|
|
160
|
-
export function createVideoErrorFallback(
|
|
161
|
-
options: CreateVideoErrorFallbackOptions
|
|
162
|
-
): (props: ErrorFallbackProps, source?: unknown) => React.ReactNode {
|
|
163
|
-
return (props: ErrorFallbackProps, source?: unknown) => (
|
|
164
|
-
<VideoErrorFallback
|
|
165
|
-
{...props}
|
|
166
|
-
downloadUrl={options.getDownloadUrl?.(source)}
|
|
167
|
-
downloadFilename={options.getFilename?.(source)}
|
|
168
|
-
fileSize={options.getFileSize?.(source)}
|
|
169
|
-
showRetry={options.showRetry}
|
|
170
|
-
/>
|
|
171
|
-
);
|
|
172
|
-
}
|