mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(sandbox): rebuild sandbox with shell UI and expanded templates (#773)
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import type { Skin } from '../../types';
|
||||
|
||||
type SkinTagMap = Record<Skin, { video: string; audio: string }>;
|
||||
|
||||
export const CSS_SKIN_TAGS: SkinTagMap = {
|
||||
default: { video: 'video-skin', audio: 'audio-skin' },
|
||||
minimal: { video: 'video-minimal-skin', audio: 'audio-minimal-skin' },
|
||||
};
|
||||
|
||||
export const TAILWIND_SKIN_TAGS: SkinTagMap = {
|
||||
default: { video: 'video-skin-tailwind', audio: 'audio-skin-tailwind' },
|
||||
minimal: { video: 'video-minimal-skin-tailwind', audio: 'audio-minimal-skin-tailwind' },
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Skin } from '../../types';
|
||||
|
||||
const videoStylesheets: Record<Skin, string> = {
|
||||
default: new URL('@videojs/html/video/skin.css', import.meta.url).href,
|
||||
minimal: new URL('@videojs/html/video/minimal-skin.css', import.meta.url).href,
|
||||
};
|
||||
|
||||
const audioStylesheets: Record<Skin, string> = {
|
||||
default: new URL('@videojs/html/audio/skin.css', import.meta.url).href,
|
||||
minimal: new URL('@videojs/html/audio/minimal-skin.css', import.meta.url).href,
|
||||
};
|
||||
|
||||
function loadStylesheet(url: string) {
|
||||
// Remove any existing link for this URL to force reload
|
||||
const existing = document.querySelector(`link[rel="stylesheet"][href="${url}"]`);
|
||||
existing?.remove();
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
export function loadVideoStylesheets(skin: Skin) {
|
||||
loadStylesheet(videoStylesheets[skin]);
|
||||
}
|
||||
|
||||
export function loadAudioStylesheets(skin: Skin) {
|
||||
loadStylesheet(audioStylesheets[skin]);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { MinimalAudioSkinTailwindElement } from '@videojs/html/audio/minimal-skin.tailwind';
|
||||
import { AudioSkinTailwindElement } from '@videojs/html/audio/skin.tailwind';
|
||||
import { MinimalVideoSkinTailwindElement } from '@videojs/html/video/minimal-skin.tailwind';
|
||||
import { VideoSkinTailwindElement } from '@videojs/html/video/skin.tailwind';
|
||||
import tailwindCSS from '../../styles.css?inline';
|
||||
|
||||
const tailwindStyles = new CSSStyleSheet();
|
||||
tailwindStyles.replaceSync(tailwindCSS);
|
||||
|
||||
export function setupVideoTailwind() {
|
||||
VideoSkinTailwindElement.styles = tailwindStyles;
|
||||
MinimalVideoSkinTailwindElement.styles = tailwindStyles;
|
||||
}
|
||||
|
||||
export function setupAudioTailwind() {
|
||||
AudioSkinTailwindElement.styles = tailwindStyles;
|
||||
MinimalAudioSkinTailwindElement.styles = tailwindStyles;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createPlayer } from '@videojs/react';
|
||||
import { audioFeatures } from '@videojs/react/audio';
|
||||
import { backgroundFeatures } from '@videojs/react/background';
|
||||
import { videoFeatures } from '@videojs/react/video';
|
||||
|
||||
export const { Provider: VideoProvider } = createPlayer({
|
||||
features: videoFeatures,
|
||||
});
|
||||
|
||||
export const { Provider: AudioProvider } = createPlayer({
|
||||
features: audioFeatures,
|
||||
});
|
||||
|
||||
export const { Provider: BackgroundVideoProvider } = createPlayer({
|
||||
features: backgroundFeatures,
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { AudioSkinProps } from '@videojs/react/audio';
|
||||
import { AudioSkin, AudioSkinTailwind, MinimalAudioSkin, MinimalAudioSkinTailwind } from '@videojs/react/audio';
|
||||
import type { VideoSkinProps } from '@videojs/react/video';
|
||||
import { MinimalVideoSkin, MinimalVideoSkinTailwind, VideoSkin, VideoSkinTailwind } from '@videojs/react/video';
|
||||
import type { Skin, Styling } from '../../types';
|
||||
|
||||
type VideoSkinComponentProps = { skin: Skin; styling: Styling } & VideoSkinProps;
|
||||
|
||||
export function VideoSkinComponent({ skin, styling, ...props }: VideoSkinComponentProps) {
|
||||
if (styling === 'tailwind') {
|
||||
switch (skin) {
|
||||
case 'default':
|
||||
return <VideoSkinTailwind {...props} />;
|
||||
case 'minimal':
|
||||
return <MinimalVideoSkinTailwind {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
switch (skin) {
|
||||
case 'default':
|
||||
return <VideoSkin {...props} />;
|
||||
case 'minimal':
|
||||
return <MinimalVideoSkin {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
type AudioSkinComponentProps = { skin: Skin; styling: Styling } & AudioSkinProps;
|
||||
|
||||
export function AudioSkinComponent({ skin, styling, ...props }: AudioSkinComponentProps) {
|
||||
if (styling === 'tailwind') {
|
||||
switch (skin) {
|
||||
case 'default':
|
||||
return <AudioSkinTailwind {...props} />;
|
||||
case 'minimal':
|
||||
return <MinimalAudioSkinTailwind {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
switch (skin) {
|
||||
case 'default':
|
||||
return <AudioSkin {...props} />;
|
||||
case 'minimal':
|
||||
return <MinimalAudioSkin {...props} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Skin } from '../../types';
|
||||
import { getInitialSkin, onSkinChange } from '../sandbox-listener';
|
||||
|
||||
export function useSkin(): Skin {
|
||||
const [skin, setSkin] = useState(getInitialSkin);
|
||||
useEffect(() => onSkinChange(setSkin), []);
|
||||
return skin;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getInitialSource, onSourceChange } from '../sandbox-listener';
|
||||
import type { SourceId } from '../sources';
|
||||
|
||||
export function useSource(audioOnly?: boolean): SourceId {
|
||||
const [source, setSource] = useState(() => getInitialSource(audioOnly));
|
||||
useEffect(() => onSourceChange(setSource), []);
|
||||
return source;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Skin } from '../types';
|
||||
import { createWebStorageStore } from '../utils/create-web-storage-store';
|
||||
import { DEFAULT_AUDIO_SOURCE, SOURCES, type SourceId } from './sources';
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
const skinStore = createWebStorageStore<Skin>('local', 'skin', 'default');
|
||||
const sourceStore = createWebStorageStore<SourceId>('local', 'source', 'hls-1');
|
||||
|
||||
// Apply query param overrides to localStorage so they persist
|
||||
const skinParam = params.get('skin') as Skin | null;
|
||||
if (skinParam) skinStore.setValue(skinParam);
|
||||
|
||||
const sourceParam = params.get('source') as SourceId | null;
|
||||
if (sourceParam) sourceStore.setValue(sourceParam);
|
||||
|
||||
export function getInitialSkin(): Skin {
|
||||
return skinStore.getSnapshot();
|
||||
}
|
||||
|
||||
export function onSkinChange(callback: (skin: Skin) => void): () => void {
|
||||
const unsubStore = skinStore.subscribe(() => callback(skinStore.getSnapshot()));
|
||||
|
||||
const handler = (event: MessageEvent) => {
|
||||
if (event.data?.type === 'skin-change' && event.data.skin !== skinStore.getSnapshot()) {
|
||||
skinStore.setValue(event.data.skin);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handler);
|
||||
|
||||
return () => {
|
||||
unsubStore();
|
||||
window.removeEventListener('message', handler);
|
||||
};
|
||||
}
|
||||
|
||||
export function getInitialSource(audioOnly?: boolean): SourceId {
|
||||
const stored = sourceStore.getSnapshot();
|
||||
|
||||
if (audioOnly && SOURCES[stored].type !== 'mp4') {
|
||||
return DEFAULT_AUDIO_SOURCE;
|
||||
}
|
||||
|
||||
return stored;
|
||||
}
|
||||
|
||||
export function onSourceChange(callback: (source: SourceId) => void): () => void {
|
||||
const unsubStore = sourceStore.subscribe(() => callback(sourceStore.getSnapshot()));
|
||||
|
||||
const handler = (event: MessageEvent) => {
|
||||
if (event.data?.type === 'source-change' && event.data.source !== sourceStore.getSnapshot()) {
|
||||
sourceStore.setValue(event.data.source);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handler);
|
||||
|
||||
return () => {
|
||||
unsubStore();
|
||||
window.removeEventListener('message', handler);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export type SourceId = 'hls-1' | 'hls-2' | 'hls-3' | 'hls-4' | 'mp4-1';
|
||||
|
||||
export const SOURCES: Record<SourceId, { label: string; url: string; type: 'hls' | 'mp4' }> = {
|
||||
'hls-1': {
|
||||
label: 'HLS - Big Buck Bunny',
|
||||
url: 'https://stream.mux.com/VcmKA6aqzIzlg3MayLJDnbF55kX00mds028Z65QxvBYaA.m3u8',
|
||||
type: 'hls',
|
||||
},
|
||||
'hls-2': {
|
||||
label: 'HLS - 2',
|
||||
url: 'https://stream.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB008.m3u8',
|
||||
type: 'hls',
|
||||
},
|
||||
'hls-3': {
|
||||
label: 'HLS - Dancing Dude',
|
||||
url: 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4.m3u8',
|
||||
type: 'hls',
|
||||
},
|
||||
'hls-4': {
|
||||
label: 'HLS - Plyr',
|
||||
url: 'https://stream.mux.com/lyrKpPcGfqyzeI00jZAfW6MvP6GNPrkML.m3u8',
|
||||
type: 'hls',
|
||||
},
|
||||
'mp4-1': {
|
||||
label: 'MP4 - Dancing Dude',
|
||||
url: 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4/highest.mp4',
|
||||
type: 'mp4',
|
||||
},
|
||||
};
|
||||
|
||||
export const SOURCE_IDS = Object.keys(SOURCES) as SourceId[];
|
||||
export const MP4_SOURCE_IDS = SOURCE_IDS.filter((id) => SOURCES[id].type === 'mp4');
|
||||
export const DEFAULT_SOURCE: SourceId = 'hls-1';
|
||||
export const DEFAULT_AUDIO_SOURCE: SourceId = 'mp4-1';
|
||||
|
||||
export const BACKGROUND_VIDEO_SRC = 'https://stream.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB008/low.mp4';
|
||||
Reference in New Issue
Block a user