mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(sandbox): dynamically load skins by styling (#989)
This commit is contained in:
@@ -1,16 +1,5 @@
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import '@app/styles.css';
|
||||
import { App } from '@app/shell/app';
|
||||
import type { Skin } from '@app/types';
|
||||
import { skinStore, sourceStore } from '@app/utils/stores';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
// Sync query-param overrides to localStorage BEFORE React mounts,
|
||||
// so useWebStorage hooks pick up the correct initial values.
|
||||
const params = new URLSearchParams(location.search);
|
||||
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);
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { getInitialSkin, getInitialSource } from '@app/shared/sandbox-listener';
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import type { Skin, Styling } from '@app/types';
|
||||
|
||||
function getInitialStyling(): Styling {
|
||||
return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
|
||||
}
|
||||
|
||||
export type HtmlSandboxState = {
|
||||
skin: Skin;
|
||||
source: SourceId;
|
||||
styling: Styling;
|
||||
};
|
||||
|
||||
export function createHtmlSandboxState(audioOnly?: boolean): HtmlSandboxState {
|
||||
return {
|
||||
skin: getInitialSkin(),
|
||||
source: getInitialSource(audioOnly),
|
||||
styling: getInitialStyling(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createLatestLoader() {
|
||||
let loadVersion = 0;
|
||||
|
||||
return async <Result>(load: () => Promise<Result>): Promise<Result | undefined> => {
|
||||
const version = ++loadVersion;
|
||||
try {
|
||||
const result = await load();
|
||||
return version === loadVersion ? result : undefined;
|
||||
} catch (error) {
|
||||
// Swallow load errors to avoid unhandled promise rejections in callers
|
||||
// that do not await the returned promise. Callers can treat `undefined`
|
||||
// as a signal that no valid result is available.
|
||||
console.error('Failed to load latest result', error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { Skin, Styling } from '@app/types';
|
||||
import { CSS_SKIN_TAGS, TAILWIND_SKIN_TAGS } from './skin-tags';
|
||||
import { loadAudioStylesheets, loadVideoStylesheets } from './stylesheets';
|
||||
|
||||
async function loadVideoCssSkin(skin: Skin): Promise<string> {
|
||||
if (skin === 'default') {
|
||||
await import('@videojs/html/video/skin');
|
||||
} else {
|
||||
await import('@videojs/html/video/minimal-skin');
|
||||
}
|
||||
|
||||
loadVideoStylesheets(skin);
|
||||
|
||||
return CSS_SKIN_TAGS[skin].video;
|
||||
}
|
||||
|
||||
async function loadAudioCssSkin(skin: Skin): Promise<string> {
|
||||
if (skin === 'default') {
|
||||
await import('@videojs/html/audio/skin');
|
||||
} else {
|
||||
await import('@videojs/html/audio/minimal-skin');
|
||||
}
|
||||
|
||||
loadAudioStylesheets(skin);
|
||||
|
||||
return CSS_SKIN_TAGS[skin].audio;
|
||||
}
|
||||
|
||||
async function loadVideoTailwindSkin(skin: Skin): Promise<string> {
|
||||
if (skin === 'default') {
|
||||
const { VideoSkinTailwindElement } = await import('@videojs/html/video/skin.tailwind');
|
||||
const { getTailwindStyles } = await import('./tailwind-setup');
|
||||
|
||||
VideoSkinTailwindElement.styles = getTailwindStyles();
|
||||
} else {
|
||||
const { MinimalVideoSkinTailwindElement } = await import('@videojs/html/video/minimal-skin.tailwind');
|
||||
const { getTailwindStyles } = await import('./tailwind-setup');
|
||||
|
||||
MinimalVideoSkinTailwindElement.styles = getTailwindStyles();
|
||||
}
|
||||
|
||||
return TAILWIND_SKIN_TAGS[skin].video;
|
||||
}
|
||||
|
||||
async function loadAudioTailwindSkin(skin: Skin): Promise<string> {
|
||||
if (skin === 'default') {
|
||||
const { AudioSkinTailwindElement } = await import('@videojs/html/audio/skin.tailwind');
|
||||
const { getTailwindStyles } = await import('./tailwind-setup');
|
||||
|
||||
AudioSkinTailwindElement.styles = getTailwindStyles();
|
||||
} else {
|
||||
const { MinimalAudioSkinTailwindElement } = await import('@videojs/html/audio/minimal-skin.tailwind');
|
||||
const { getTailwindStyles } = await import('./tailwind-setup');
|
||||
|
||||
MinimalAudioSkinTailwindElement.styles = getTailwindStyles();
|
||||
}
|
||||
|
||||
return TAILWIND_SKIN_TAGS[skin].audio;
|
||||
}
|
||||
|
||||
export function loadVideoSkinTag(skin: Skin, styling: Styling): Promise<string> {
|
||||
return styling === 'tailwind' ? loadVideoTailwindSkin(skin) : loadVideoCssSkin(skin);
|
||||
}
|
||||
|
||||
export function loadAudioSkinTag(skin: Skin, styling: Styling): Promise<string> {
|
||||
return styling === 'tailwind' ? loadAudioTailwindSkin(skin) : loadAudioCssSkin(skin);
|
||||
}
|
||||
@@ -10,21 +10,21 @@ const audioStylesheets: Record<Skin, string> = {
|
||||
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}"]`);
|
||||
function loadStylesheet(id: string, url: string) {
|
||||
const existing = document.querySelector(`link[rel="stylesheet"][data-sandbox-stylesheet="${id}"]`);
|
||||
existing?.remove();
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.dataset.sandboxStylesheet = id;
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
export function loadVideoStylesheets(skin: Skin) {
|
||||
loadStylesheet(videoStylesheets[skin]);
|
||||
loadStylesheet('video-skin', videoStylesheets[skin]);
|
||||
}
|
||||
|
||||
export function loadAudioStylesheets(skin: Skin) {
|
||||
loadStylesheet(audioStylesheets[skin]);
|
||||
loadStylesheet('audio-skin', audioStylesheets[skin]);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import tailwindCSS from '@app/styles.css?inline';
|
||||
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';
|
||||
|
||||
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;
|
||||
export function getTailwindStyles(): CSSStyleSheet {
|
||||
return tailwindStyles;
|
||||
}
|
||||
|
||||
@@ -1,45 +1,87 @@
|
||||
import type { Skin, Styling } from '@app/types';
|
||||
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 { ComponentType } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
async function loadVideoSkinComponent(skin: Skin, styling: Styling): Promise<ComponentType<VideoSkinProps>> {
|
||||
const module = await import('@videojs/react/video');
|
||||
|
||||
if (styling === 'tailwind') {
|
||||
return skin === 'default' ? module.VideoSkinTailwind : module.MinimalVideoSkinTailwind;
|
||||
}
|
||||
|
||||
if (skin === 'default') {
|
||||
await import('@videojs/react/video/skin.css');
|
||||
return module.VideoSkin;
|
||||
}
|
||||
|
||||
await import('@videojs/react/video/minimal-skin.css');
|
||||
return module.MinimalVideoSkin;
|
||||
}
|
||||
|
||||
async function loadAudioSkinComponent(skin: Skin, styling: Styling): Promise<ComponentType<AudioSkinProps>> {
|
||||
const module = await import('@videojs/react/audio');
|
||||
|
||||
if (styling === 'tailwind') {
|
||||
return skin === 'default' ? module.AudioSkinTailwind : module.MinimalAudioSkinTailwind;
|
||||
}
|
||||
|
||||
if (skin === 'default') {
|
||||
await import('@videojs/react/audio/skin.css');
|
||||
return module.AudioSkin;
|
||||
}
|
||||
|
||||
await import('@videojs/react/audio/minimal-skin.css');
|
||||
return module.MinimalAudioSkin;
|
||||
}
|
||||
|
||||
function useLoadedComponent<Props>(
|
||||
load: () => Promise<ComponentType<Props>>,
|
||||
deps: readonly unknown[]
|
||||
): ComponentType<Props> | null {
|
||||
const [component, setComponent] = useState<ComponentType<Props> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
|
||||
void load()
|
||||
.then((resolved) => {
|
||||
if (!active) return;
|
||||
|
||||
setComponent(() => resolved);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!active) return;
|
||||
// Intentionally ignore load errors to avoid unhandled promise rejections.
|
||||
// The component will remain null, and callers can handle absence as needed.
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: we're proxying the deps
|
||||
}, deps);
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
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} />;
|
||||
}
|
||||
}
|
||||
const Component = useLoadedComponent(() => loadVideoSkinComponent(skin, styling), [skin, styling]);
|
||||
|
||||
switch (skin) {
|
||||
case 'default':
|
||||
return <VideoSkin {...props} />;
|
||||
case 'minimal':
|
||||
return <MinimalVideoSkin {...props} />;
|
||||
}
|
||||
if (!Component) return null;
|
||||
|
||||
return <Component {...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} />;
|
||||
}
|
||||
}
|
||||
const Component = useLoadedComponent(() => loadAudioSkinComponent(skin, styling), [skin, styling]);
|
||||
|
||||
switch (skin) {
|
||||
case 'default':
|
||||
return <AudioSkin {...props} />;
|
||||
case 'minimal':
|
||||
return <MinimalAudioSkin {...props} />;
|
||||
}
|
||||
if (!Component) return null;
|
||||
|
||||
return <Component {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,42 +1,45 @@
|
||||
import { SKINS } from '@app/constants';
|
||||
import type { Skin } from '@app/types';
|
||||
import { createWebStorageStore } from '@app/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');
|
||||
function readSkin(): Skin {
|
||||
const skin = params.get('skin');
|
||||
|
||||
// Apply query param overrides to localStorage so they persist
|
||||
const skinParam = params.get('skin') as Skin | null;
|
||||
if (skinParam) skinStore.setValue(skinParam);
|
||||
return skin && SKINS.includes(skin as Skin) ? (skin as Skin) : 'default';
|
||||
}
|
||||
|
||||
const sourceParam = params.get('source') as SourceId | null;
|
||||
if (sourceParam) sourceStore.setValue(sourceParam);
|
||||
function readSource(): SourceId {
|
||||
const source = params.get('source');
|
||||
|
||||
return source && source in SOURCES ? (source as SourceId) : 'hls-1';
|
||||
}
|
||||
|
||||
let currentSkin = readSkin();
|
||||
let currentSource = readSource();
|
||||
|
||||
export function getInitialSkin(): Skin {
|
||||
return skinStore.getSnapshot();
|
||||
return currentSkin;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if (event.data?.type !== 'skin-change' || !SKINS.includes(event.data.skin)) return;
|
||||
|
||||
currentSkin = event.data.skin;
|
||||
callback(currentSkin);
|
||||
};
|
||||
|
||||
window.addEventListener('message', handler);
|
||||
|
||||
return () => {
|
||||
unsubStore();
|
||||
window.removeEventListener('message', handler);
|
||||
};
|
||||
}
|
||||
|
||||
export function getInitialSource(audioOnly?: boolean): SourceId {
|
||||
const stored = sourceStore.getSnapshot();
|
||||
const stored = currentSource;
|
||||
|
||||
if (audioOnly && SOURCES[stored].type !== 'mp4') {
|
||||
return DEFAULT_AUDIO_SOURCE;
|
||||
@@ -46,18 +49,16 @@ export function getInitialSource(audioOnly?: boolean): SourceId {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if (event.data?.type !== 'source-change' || !(event.data.source in SOURCES)) return;
|
||||
|
||||
currentSource = event.data.source;
|
||||
callback(currentSource);
|
||||
};
|
||||
|
||||
window.addEventListener('message', handler);
|
||||
|
||||
return () => {
|
||||
unsubStore();
|
||||
window.removeEventListener('message', handler);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,15 +9,12 @@ import {
|
||||
SOURCES,
|
||||
} from '@app/shared/sources';
|
||||
import type { Platform, Preset, Styling } from '@app/types';
|
||||
import { useSkinSwitcher } from '@app/utils/use-skin-switcher';
|
||||
import { useSourceSwitcher } from '@app/utils/use-source-switcher';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Navbar } from './navbar';
|
||||
import { Preview } from './preview';
|
||||
|
||||
function getPagePath(platform: Platform, styling: Styling, preset: Preset): string {
|
||||
function getPagePath(platform: Platform, preset: Preset): string {
|
||||
if (preset === 'background-video') return `/${platform}-background-video/`;
|
||||
if (styling === 'tailwind') return `/${platform}-${preset}-tailwind/`;
|
||||
return `/${platform}-${preset}/`;
|
||||
}
|
||||
|
||||
@@ -27,19 +24,21 @@ function readParams() {
|
||||
platform: (params.get('platform') ?? 'html') as Platform,
|
||||
styling: (params.get('styling') ?? 'css') as Styling,
|
||||
preset: (params.get('preset') ?? 'video') as Preset,
|
||||
skin: (params.get('skin') ?? 'default') as 'default' | 'minimal',
|
||||
source: (params.get('source') ?? 'hls-1') as SourceId,
|
||||
};
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const initial = useMemo(readParams, []);
|
||||
const [platform, setPlatform] = useState<Platform>(initial.platform);
|
||||
const [styling, setStyling] = useState<Styling>(initial.styling);
|
||||
const [styling, setStyling] = useState(initial.styling);
|
||||
const [preset, setPreset] = useState<Preset>(initial.preset);
|
||||
const [skin, setSkin] = useSkinSwitcher();
|
||||
const [source, setSource] = useSourceSwitcher();
|
||||
const [skin, setSkin] = useState(initial.skin);
|
||||
const [source, setSource] = useState(initial.source);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const pagePath = getPagePath(platform, styling, preset);
|
||||
const pagePath = getPagePath(platform, preset);
|
||||
|
||||
// Keep the URL in sync with all state (including skin + source)
|
||||
useEffect(() => {
|
||||
@@ -47,23 +46,11 @@ export function App() {
|
||||
history.replaceState(null, '', `/?${params}`);
|
||||
}, [platform, styling, preset, skin, source]);
|
||||
|
||||
// Send postMessage to iframe for skin changes (skip initial mount — iframe reads localStorage)
|
||||
const skinMountRef = useRef(true);
|
||||
useEffect(() => {
|
||||
if (skinMountRef.current) {
|
||||
skinMountRef.current = false;
|
||||
return;
|
||||
}
|
||||
iframeRef.current?.contentWindow?.postMessage({ type: 'skin-change', skin }, '*');
|
||||
}, [skin]);
|
||||
|
||||
// Send postMessage to iframe for source changes (skip initial mount — iframe reads localStorage)
|
||||
const sourceMountRef = useRef(true);
|
||||
useEffect(() => {
|
||||
if (sourceMountRef.current) {
|
||||
sourceMountRef.current = false;
|
||||
return;
|
||||
}
|
||||
iframeRef.current?.contentWindow?.postMessage({ type: 'source-change', source }, '*');
|
||||
}, [source]);
|
||||
|
||||
@@ -81,9 +68,9 @@ export function App() {
|
||||
}
|
||||
}, [preset, source, setSource]);
|
||||
|
||||
// Constrain styling when switching to a preset that has no tailwind template
|
||||
// Background video does not have a Tailwind skin variant.
|
||||
useEffect(() => {
|
||||
if ((preset === 'background-video' || preset === 'dash-video') && styling === 'tailwind') {
|
||||
if (preset === 'background-video' && styling === 'tailwind') {
|
||||
setStyling('css');
|
||||
}
|
||||
}, [preset, styling]);
|
||||
@@ -108,13 +95,19 @@ export function App() {
|
||||
availableSources={availableSources}
|
||||
isBackgroundVideo={preset === 'background-video'}
|
||||
isSimpleHlsVideo={preset === 'simple-hls-video'}
|
||||
isDashVideo={preset === 'dash-video'}
|
||||
platforms={PLATFORMS}
|
||||
stylings={STYLINGS}
|
||||
presets={PRESETS}
|
||||
sources={SOURCES}
|
||||
/>
|
||||
<Preview ref={iframeRef} pagePath={pagePath} skin={skin} source={source} />
|
||||
<Preview
|
||||
key={`${pagePath}:${styling}`}
|
||||
ref={iframeRef}
|
||||
pagePath={pagePath}
|
||||
skin={skin}
|
||||
styling={styling}
|
||||
source={source}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ type NavbarProps = {
|
||||
availableSources: readonly SourceId[];
|
||||
isBackgroundVideo: boolean;
|
||||
isSimpleHlsVideo: boolean;
|
||||
isDashVideo: boolean;
|
||||
platforms: readonly Platform[];
|
||||
stylings: readonly Styling[];
|
||||
presets: readonly Preset[];
|
||||
@@ -53,7 +52,6 @@ export function Navbar({
|
||||
availableSources,
|
||||
isBackgroundVideo,
|
||||
isSimpleHlsVideo,
|
||||
isDashVideo,
|
||||
platforms,
|
||||
stylings,
|
||||
presets,
|
||||
@@ -82,7 +80,7 @@ export function Navbar({
|
||||
options={stylings.map((s) => ({
|
||||
value: s,
|
||||
label: s === 'css' ? 'CSS' : 'Tailwind',
|
||||
disabled: s === 'tailwind' && (isBackgroundVideo || isDashVideo),
|
||||
disabled: s === 'tailwind' && isBackgroundVideo,
|
||||
}))}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import type { Skin } from '@app/types';
|
||||
import { forwardRef } from 'react';
|
||||
import type { Skin, Styling } from '@app/types';
|
||||
import { forwardRef, useState } from 'react';
|
||||
|
||||
type PreviewProps = {
|
||||
pagePath: string;
|
||||
skin: Skin;
|
||||
styling: Styling;
|
||||
source: SourceId;
|
||||
};
|
||||
|
||||
export const Preview = forwardRef<HTMLIFrameElement, PreviewProps>(function Preview({ pagePath, skin, source }, ref) {
|
||||
const openUrl = `${pagePath}?skin=${encodeURIComponent(skin)}&source=${encodeURIComponent(source)}`;
|
||||
export const Preview = forwardRef<HTMLIFrameElement, PreviewProps>(function Preview(
|
||||
{ pagePath, skin, styling, source },
|
||||
ref
|
||||
) {
|
||||
const [iframeUrl] = useState(
|
||||
() =>
|
||||
`${pagePath}?skin=${encodeURIComponent(skin)}&styling=${encodeURIComponent(styling)}&source=${encodeURIComponent(source)}`
|
||||
);
|
||||
const openUrl =
|
||||
`${pagePath}?skin=${encodeURIComponent(skin)}&styling=${encodeURIComponent(styling)}` +
|
||||
`&source=${encodeURIComponent(source)}`;
|
||||
|
||||
return (
|
||||
<main className="flex-1 min-h-0 relative bg-zinc-50 dark:bg-zinc-900">
|
||||
@@ -37,7 +47,7 @@ export const Preview = forwardRef<HTMLIFrameElement, PreviewProps>(function Prev
|
||||
<line x1="10" x2="21" y1="14" y2="3" />
|
||||
</svg>
|
||||
</a>
|
||||
<iframe ref={ref} src={pagePath} className="absolute inset-0 w-full h-full border-0" title="player demo" />
|
||||
<iframe ref={ref} src={iframeUrl} className="absolute inset-0 w-full h-full border-0" title="player demo" />
|
||||
</main>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
export type WebStorageType = 'local' | 'session';
|
||||
|
||||
export type WebStorageSerializableValue =
|
||||
| string
|
||||
| boolean
|
||||
| number
|
||||
| null
|
||||
| Record<string, any>
|
||||
| WebStorageSerializableValue[];
|
||||
|
||||
type Subscriber = () => void;
|
||||
|
||||
export function createWebStorageStore<T extends WebStorageSerializableValue>(
|
||||
type: WebStorageType,
|
||||
key: string,
|
||||
initialValue: T
|
||||
) {
|
||||
const listeners = new Set<Subscriber>();
|
||||
|
||||
const getSnapshot = (): T => {
|
||||
const data = type === 'local' ? localStorage.getItem(key) : sessionStorage.getItem(key);
|
||||
return data ? JSON.parse(data) : initialValue;
|
||||
};
|
||||
|
||||
const setValue = (value: T) => {
|
||||
if (type === 'local') {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} else {
|
||||
sessionStorage.setItem(key, JSON.stringify(value));
|
||||
}
|
||||
listeners.forEach((l) => l());
|
||||
};
|
||||
|
||||
const subscribe = (listener: Subscriber) => {
|
||||
listeners.add(listener);
|
||||
|
||||
const onStorage = (event: StorageEvent) => {
|
||||
if (event.key === key) listener();
|
||||
};
|
||||
|
||||
window.addEventListener('storage', onStorage);
|
||||
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
window.removeEventListener('storage', onStorage);
|
||||
};
|
||||
};
|
||||
|
||||
return { getSnapshot, setValue, subscribe };
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import type { Skin } from '@app/types';
|
||||
import { createWebStorageStore } from './create-web-storage-store';
|
||||
|
||||
export const skinStore = createWebStorageStore<Skin>('local', 'skin', 'default');
|
||||
export const sourceStore = createWebStorageStore<SourceId>('local', 'source', 'hls-1');
|
||||
@@ -1,5 +0,0 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
export function useExternalStore<T>(store: { subscribe: (listener: () => void) => () => void; getSnapshot: () => T }) {
|
||||
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { Skin } from '@app/types';
|
||||
import { skinStore } from './stores';
|
||||
import { useExternalStore } from './use-external-store';
|
||||
|
||||
export function useSkinSwitcher(): [Skin, (value: Skin) => void] {
|
||||
const value = useExternalStore(skinStore);
|
||||
return [value, skinStore.setValue];
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import { sourceStore } from './stores';
|
||||
import { useExternalStore } from './use-external-store';
|
||||
|
||||
export function useSourceSwitcher(): [SourceId, (value: SourceId) => void] {
|
||||
const value = useExternalStore(sourceStore);
|
||||
return [value, sourceStore.setValue];
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
createWebStorageStore,
|
||||
type WebStorageSerializableValue,
|
||||
type WebStorageType,
|
||||
} from './create-web-storage-store';
|
||||
import { useExternalStore } from './use-external-store';
|
||||
|
||||
export function useWebStorage<T extends WebStorageSerializableValue>(
|
||||
type: WebStorageType,
|
||||
key: string,
|
||||
defaultValue: T
|
||||
): [T, (value: T) => void] {
|
||||
const [store] = useState(() => createWebStorageStore<T>(type, key, defaultValue));
|
||||
const value = useExternalStore(store);
|
||||
return [value, store.setValue];
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sandbox — HTML Audio Tailwind</title>
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
</head>
|
||||
<body class="font-sans">
|
||||
<div id="root" class="flex justify-center items-center min-h-screen"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,43 +0,0 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/html/audio/player';
|
||||
import '@videojs/html/audio/skin.tailwind';
|
||||
import '@videojs/html/audio/minimal-skin.tailwind';
|
||||
import { TAILWIND_SKIN_TAGS } from '@app/shared/html/skin-tags';
|
||||
import { setupAudioTailwind } from '@app/shared/html/tailwind-setup';
|
||||
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Skin } from '@app/types';
|
||||
|
||||
setupAudioTailwind();
|
||||
|
||||
const html = String.raw;
|
||||
|
||||
let currentSkin: Skin = getInitialSkin();
|
||||
let currentSource: SourceId = getInitialSource(true);
|
||||
|
||||
function render() {
|
||||
const tag = TAILWIND_SKIN_TAGS[currentSkin].audio;
|
||||
|
||||
document.getElementById('root')!.innerHTML = html`
|
||||
<div class="w-full max-w-xl mx-auto">
|
||||
<audio-player>
|
||||
<${tag}>
|
||||
<audio slot="media" src="${SOURCES[currentSource].url}"></audio>
|
||||
</${tag}>
|
||||
</audio-player>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
onSkinChange((skin) => {
|
||||
currentSkin = skin;
|
||||
render();
|
||||
});
|
||||
|
||||
onSourceChange((source) => {
|
||||
currentSource = source;
|
||||
render();
|
||||
});
|
||||
@@ -1,29 +1,24 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/html/audio/player';
|
||||
import '@videojs/html/audio/skin';
|
||||
import '@videojs/html/audio/minimal-skin';
|
||||
import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
|
||||
import { loadAudioStylesheets } from '@app/shared/html/stylesheets';
|
||||
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import { createHtmlSandboxState, createLatestLoader } from '@app/shared/html/sandbox-state';
|
||||
import { loadAudioSkinTag } from '@app/shared/html/skins';
|
||||
import { onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Skin } from '@app/types';
|
||||
|
||||
const html = String.raw;
|
||||
|
||||
let currentSkin: Skin = getInitialSkin();
|
||||
let currentSource: SourceId = getInitialSource(true);
|
||||
const state = createHtmlSandboxState(true);
|
||||
const loadLatest = createLatestLoader();
|
||||
|
||||
function render() {
|
||||
const tag = CSS_SKIN_TAGS[currentSkin].audio;
|
||||
|
||||
loadAudioStylesheets(currentSkin);
|
||||
async function render() {
|
||||
const tag = await loadLatest(() => loadAudioSkinTag(state.skin, state.styling));
|
||||
if (!tag) return;
|
||||
|
||||
document.getElementById('root')!.innerHTML = html`
|
||||
<div class="w-full max-w-xl mx-auto">
|
||||
<audio-player>
|
||||
<${tag}>
|
||||
<audio slot="media" src="${SOURCES[currentSource].url}"></audio>
|
||||
<audio slot="media" src="${SOURCES[state.source].url}"></audio>
|
||||
</${tag}>
|
||||
</audio-player>
|
||||
</div>
|
||||
@@ -33,11 +28,11 @@ function render() {
|
||||
render();
|
||||
|
||||
onSkinChange((skin) => {
|
||||
currentSkin = skin;
|
||||
state.skin = skin;
|
||||
render();
|
||||
});
|
||||
|
||||
onSourceChange((source) => {
|
||||
currentSource = source;
|
||||
state.source = source;
|
||||
render();
|
||||
});
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/html/video/player';
|
||||
import '@videojs/html/media/dash-video';
|
||||
import '@videojs/html/video/skin';
|
||||
import '@videojs/html/video/minimal-skin';
|
||||
import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
|
||||
import { loadVideoStylesheets } from '@app/shared/html/stylesheets';
|
||||
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import { createHtmlSandboxState, createLatestLoader } from '@app/shared/html/sandbox-state';
|
||||
import { loadVideoSkinTag } from '@app/shared/html/skins';
|
||||
import { onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Skin } from '@app/types';
|
||||
|
||||
const html = String.raw;
|
||||
|
||||
let currentSkin: Skin = getInitialSkin();
|
||||
let currentSource: SourceId = getInitialSource();
|
||||
const state = createHtmlSandboxState();
|
||||
const loadLatest = createLatestLoader();
|
||||
|
||||
function render() {
|
||||
const tag = CSS_SKIN_TAGS[currentSkin].video;
|
||||
|
||||
loadVideoStylesheets(currentSkin);
|
||||
async function render() {
|
||||
const tag = await loadLatest(() => loadVideoSkinTag(state.skin, state.styling));
|
||||
if (!tag) return;
|
||||
|
||||
document.getElementById('root')!.innerHTML = html`
|
||||
<video-player>
|
||||
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
|
||||
<dash-video slot="media" src="${SOURCES[currentSource].url}" playsinline></dash-video>
|
||||
<dash-video slot="media" src="${SOURCES[state.source].url}" playsinline></dash-video>
|
||||
</${tag}>
|
||||
</video-player>
|
||||
`;
|
||||
@@ -32,11 +27,11 @@ function render() {
|
||||
render();
|
||||
|
||||
onSkinChange((skin) => {
|
||||
currentSkin = skin;
|
||||
state.skin = skin;
|
||||
render();
|
||||
});
|
||||
|
||||
onSourceChange((source) => {
|
||||
currentSource = source;
|
||||
state.source = source;
|
||||
render();
|
||||
});
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/html/video/player';
|
||||
import '@videojs/html/media/hls-video';
|
||||
import '@videojs/html/video/skin';
|
||||
import '@videojs/html/video/minimal-skin';
|
||||
import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
|
||||
import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
|
||||
import { loadVideoStylesheets } from '@app/shared/html/stylesheets';
|
||||
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import { createHtmlSandboxState, createLatestLoader } from '@app/shared/html/sandbox-state';
|
||||
import { loadVideoSkinTag } from '@app/shared/html/skins';
|
||||
import { onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Skin } from '@app/types';
|
||||
|
||||
const html = String.raw;
|
||||
|
||||
let currentSkin: Skin = getInitialSkin();
|
||||
let currentSource: SourceId = getInitialSource();
|
||||
const state = createHtmlSandboxState();
|
||||
const loadLatest = createLatestLoader();
|
||||
|
||||
function render() {
|
||||
const tag = CSS_SKIN_TAGS[currentSkin].video;
|
||||
|
||||
loadVideoStylesheets(currentSkin);
|
||||
async function render() {
|
||||
const tag = await loadLatest(() => loadVideoSkinTag(state.skin, state.styling));
|
||||
if (!tag) return;
|
||||
|
||||
document.getElementById('root')!.innerHTML = html`
|
||||
<video-player>
|
||||
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
|
||||
<hls-video slot="media" src="${SOURCES[currentSource].url}" playsinline crossorigin="anonymous">
|
||||
${renderMuxStoryboard(currentSource)}
|
||||
<hls-video slot="media" src="${SOURCES[state.source].url}" playsinline crossorigin="anonymous">
|
||||
${renderMuxStoryboard(state.source)}
|
||||
</hls-video>
|
||||
</${tag}>
|
||||
</video-player>
|
||||
@@ -35,11 +30,11 @@ function render() {
|
||||
render();
|
||||
|
||||
onSkinChange((skin) => {
|
||||
currentSkin = skin;
|
||||
state.skin = skin;
|
||||
render();
|
||||
});
|
||||
|
||||
onSourceChange((source) => {
|
||||
currentSource = source;
|
||||
state.source = source;
|
||||
render();
|
||||
});
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/html/video/player';
|
||||
import '@videojs/html/media/simple-hls-video';
|
||||
import '@videojs/html/video/skin';
|
||||
import '@videojs/html/video/minimal-skin';
|
||||
import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
|
||||
import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
|
||||
import { loadVideoStylesheets } from '@app/shared/html/stylesheets';
|
||||
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import { createHtmlSandboxState, createLatestLoader } from '@app/shared/html/sandbox-state';
|
||||
import { loadVideoSkinTag } from '@app/shared/html/skins';
|
||||
import { onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Skin } from '@app/types';
|
||||
|
||||
const html = String.raw;
|
||||
|
||||
let currentSkin: Skin = getInitialSkin();
|
||||
let currentSource: SourceId = getInitialSource();
|
||||
const state = createHtmlSandboxState();
|
||||
const loadLatest = createLatestLoader();
|
||||
|
||||
function render() {
|
||||
const tag = CSS_SKIN_TAGS[currentSkin].video;
|
||||
|
||||
loadVideoStylesheets(currentSkin);
|
||||
async function render() {
|
||||
const tag = await loadLatest(() => loadVideoSkinTag(state.skin, state.styling));
|
||||
if (!tag) return;
|
||||
|
||||
document.getElementById('root')!.innerHTML = html`
|
||||
<video-player>
|
||||
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
|
||||
<simple-hls-video slot="media" src="${SOURCES[currentSource].url}" playsinline crossorigin="anonymous">
|
||||
${renderMuxStoryboard(currentSource)}
|
||||
<simple-hls-video slot="media" src="${SOURCES[state.source].url}" playsinline crossorigin="anonymous">
|
||||
${renderMuxStoryboard(state.source)}
|
||||
</simple-hls-video>
|
||||
</${tag}>
|
||||
</video-player>
|
||||
@@ -35,11 +30,11 @@ function render() {
|
||||
render();
|
||||
|
||||
onSkinChange((skin) => {
|
||||
currentSkin = skin;
|
||||
state.skin = skin;
|
||||
render();
|
||||
});
|
||||
|
||||
onSourceChange((source) => {
|
||||
currentSource = source;
|
||||
state.source = source;
|
||||
render();
|
||||
});
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sandbox — HTML Video Tailwind</title>
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
</head>
|
||||
<body class="font-sans">
|
||||
<div id="root" class="flex justify-center items-center min-h-screen"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,45 +0,0 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/html/ui/poster';
|
||||
import '@videojs/html/video/player';
|
||||
import '@videojs/html/video/skin.tailwind';
|
||||
import '@videojs/html/video/minimal-skin.tailwind';
|
||||
import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
|
||||
import { TAILWIND_SKIN_TAGS } from '@app/shared/html/skin-tags';
|
||||
import { setupVideoTailwind } from '@app/shared/html/tailwind-setup';
|
||||
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Skin } from '@app/types';
|
||||
|
||||
setupVideoTailwind();
|
||||
|
||||
const html = String.raw;
|
||||
|
||||
let currentSkin: Skin = getInitialSkin();
|
||||
let currentSource: SourceId = getInitialSource();
|
||||
|
||||
function render() {
|
||||
const tag = TAILWIND_SKIN_TAGS[currentSkin].video;
|
||||
|
||||
document.getElementById('root')!.innerHTML = html`
|
||||
<video-player>
|
||||
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
|
||||
<video slot="media" src="${SOURCES[currentSource].url}" playsinline crossorigin="anonymous">
|
||||
${renderMuxStoryboard(currentSource)}
|
||||
</video>
|
||||
</${tag}>
|
||||
</video-player>
|
||||
`;
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
onSkinChange((skin) => {
|
||||
currentSkin = skin;
|
||||
render();
|
||||
});
|
||||
|
||||
onSourceChange((source) => {
|
||||
currentSource = source;
|
||||
render();
|
||||
});
|
||||
@@ -1,31 +1,26 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/html/video/player';
|
||||
import '@videojs/html/video/skin';
|
||||
import '@videojs/html/video/minimal-skin';
|
||||
import '@videojs/html/ui/poster';
|
||||
import { renderMuxStoryboard } from '@app/shared/html/mux-storyboard';
|
||||
import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
|
||||
import { loadVideoStylesheets } from '@app/shared/html/stylesheets';
|
||||
import { createHtmlSandboxState, createLatestLoader } from '@app/shared/html/sandbox-state';
|
||||
import { loadVideoSkinTag } from '@app/shared/html/skins';
|
||||
import { getInitialSkin, getInitialSource, onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Skin } from '@app/types';
|
||||
|
||||
const html = String.raw;
|
||||
|
||||
let currentSkin: Skin = getInitialSkin();
|
||||
let currentSource: SourceId = getInitialSource();
|
||||
const state = createHtmlSandboxState();
|
||||
const loadLatest = createLatestLoader();
|
||||
|
||||
function render() {
|
||||
const tag = CSS_SKIN_TAGS[currentSkin].video;
|
||||
|
||||
loadVideoStylesheets(currentSkin);
|
||||
async function render() {
|
||||
const tag = await loadLatest(() => loadVideoSkinTag(state.skin, state.styling));
|
||||
if (!tag) return;
|
||||
|
||||
document.getElementById('root')!.innerHTML = html`
|
||||
<video-player>
|
||||
<${tag} class="w-full aspect-video max-w-4xl mx-auto">
|
||||
<video slot="media" src="${SOURCES[currentSource].url}" playsinline crossorigin="anonymous">
|
||||
${renderMuxStoryboard(currentSource)}
|
||||
<video slot="media" src="${SOURCES[state.source].url}" playsinline crossorigin="anonymous">
|
||||
${renderMuxStoryboard(state.source)}
|
||||
</video>
|
||||
</${tag}>
|
||||
</video-player>
|
||||
@@ -35,11 +30,11 @@ function render() {
|
||||
render();
|
||||
|
||||
onSkinChange((skin) => {
|
||||
currentSkin = skin;
|
||||
state.skin = skin;
|
||||
render();
|
||||
});
|
||||
|
||||
onSourceChange((source) => {
|
||||
currentSource = source;
|
||||
state.source = source;
|
||||
render();
|
||||
});
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sandbox — React Audio Tailwind</title>
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
</head>
|
||||
<body class="font-sans">
|
||||
<div id="root" class="flex justify-center items-center min-h-screen"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
import '@app/styles.css';
|
||||
import { AudioProvider } from '@app/shared/react/providers';
|
||||
import { AudioSkinComponent } from '@app/shared/react/skins';
|
||||
import { useSkin } from '@app/shared/react/use-skin';
|
||||
import { useSource } from '@app/shared/react/use-source';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import { Audio } from '@videojs/react/audio';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
function App() {
|
||||
const skin = useSkin();
|
||||
const source = useSource(true);
|
||||
|
||||
return (
|
||||
<AudioProvider>
|
||||
<AudioSkinComponent skin={skin} styling="tailwind" className="w-full max-w-xl mx-auto">
|
||||
<Audio src={SOURCES[source].url} />
|
||||
</AudioSkinComponent>
|
||||
</AudioProvider>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />);
|
||||
@@ -1,21 +1,26 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/react/audio/skin.css';
|
||||
import '@videojs/react/audio/minimal-skin.css';
|
||||
import { AudioProvider } from '@app/shared/react/providers';
|
||||
import { AudioSkinComponent } from '@app/shared/react/skins';
|
||||
import { useSkin } from '@app/shared/react/use-skin';
|
||||
import { useSource } from '@app/shared/react/use-source';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Styling } from '@app/types';
|
||||
import { Audio } from '@videojs/react/audio';
|
||||
import { useMemo } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
function readStyling(): Styling {
|
||||
return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
|
||||
}
|
||||
|
||||
function App() {
|
||||
const skin = useSkin();
|
||||
const source = useSource(true);
|
||||
const styling = useMemo(readStyling, []);
|
||||
|
||||
return (
|
||||
<AudioProvider>
|
||||
<AudioSkinComponent skin={skin} styling="css" className="w-full max-w-xl mx-auto">
|
||||
<AudioSkinComponent skin={skin} styling={styling} className="w-full max-w-xl mx-auto">
|
||||
<Audio src={SOURCES[source].url} />
|
||||
</AudioSkinComponent>
|
||||
</AudioProvider>
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/react/video/skin.css';
|
||||
import '@videojs/react/video/minimal-skin.css';
|
||||
import { VideoProvider } from '@app/shared/react/providers';
|
||||
import { VideoSkinComponent } from '@app/shared/react/skins';
|
||||
import { useSkin } from '@app/shared/react/use-skin';
|
||||
import { useSource } from '@app/shared/react/use-source';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Styling } from '@app/types';
|
||||
import { DashVideo } from '@videojs/react/media/dash-video';
|
||||
import { useMemo } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
function readStyling(): Styling {
|
||||
return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
|
||||
}
|
||||
|
||||
function App() {
|
||||
const skin = useSkin();
|
||||
const source = useSource();
|
||||
const styling = useMemo(readStyling, []);
|
||||
|
||||
return (
|
||||
<VideoProvider>
|
||||
<VideoSkinComponent skin={skin} styling="css" className="w-full aspect-video max-w-4xl mx-auto">
|
||||
<VideoSkinComponent skin={skin} styling={styling} className="w-full aspect-video max-w-4xl mx-auto">
|
||||
<DashVideo src={SOURCES[source].url} playsInline />
|
||||
</VideoSkinComponent>
|
||||
</VideoProvider>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/react/video/skin.css';
|
||||
import '@videojs/react/video/minimal-skin.css';
|
||||
import { MuxPoster } from '@app/shared/react/mux-poster';
|
||||
import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
|
||||
import { VideoProvider } from '@app/shared/react/providers';
|
||||
@@ -8,16 +6,23 @@ import { VideoSkinComponent } from '@app/shared/react/skins';
|
||||
import { useSkin } from '@app/shared/react/use-skin';
|
||||
import { useSource } from '@app/shared/react/use-source';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Styling } from '@app/types';
|
||||
import { HlsVideo } from '@videojs/react/media/hls-video';
|
||||
import { useMemo } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
function readStyling(): Styling {
|
||||
return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
|
||||
}
|
||||
|
||||
function App() {
|
||||
const skin = useSkin();
|
||||
const source = useSource();
|
||||
const styling = useMemo(readStyling, []);
|
||||
|
||||
return (
|
||||
<VideoProvider>
|
||||
<VideoSkinComponent skin={skin} styling="css" className="w-full aspect-video max-w-4xl mx-auto">
|
||||
<VideoSkinComponent skin={skin} styling={styling} className="w-full aspect-video max-w-4xl mx-auto">
|
||||
<HlsVideo src={SOURCES[source].url} playsInline crossOrigin="anonymous">
|
||||
<MuxStoryboard source={source} />
|
||||
</HlsVideo>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/react/video/skin.css';
|
||||
import '@videojs/react/video/minimal-skin.css';
|
||||
import { MuxPoster } from '@app/shared/react/mux-poster';
|
||||
import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
|
||||
import { VideoProvider } from '@app/shared/react/providers';
|
||||
@@ -8,16 +6,23 @@ import { VideoSkinComponent } from '@app/shared/react/skins';
|
||||
import { useSkin } from '@app/shared/react/use-skin';
|
||||
import { useSource } from '@app/shared/react/use-source';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Styling } from '@app/types';
|
||||
import { SimpleHlsVideo } from '@videojs/react/media/simple-hls-video';
|
||||
import { useMemo } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
function readStyling(): Styling {
|
||||
return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
|
||||
}
|
||||
|
||||
function App() {
|
||||
const skin = useSkin();
|
||||
const source = useSource();
|
||||
const styling = useMemo(readStyling, []);
|
||||
|
||||
return (
|
||||
<VideoProvider>
|
||||
<VideoSkinComponent skin={skin} styling="css" className="w-full aspect-video max-w-4xl mx-auto">
|
||||
<VideoSkinComponent skin={skin} styling={styling} className="w-full aspect-video max-w-4xl mx-auto">
|
||||
<SimpleHlsVideo src={SOURCES[source].url} playsInline crossOrigin="anonymous">
|
||||
<MuxStoryboard source={source} />
|
||||
</SimpleHlsVideo>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sandbox — React Video Tailwind</title>
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
</head>
|
||||
<body class="font-sans">
|
||||
<div id="root" class="flex justify-center items-center min-h-screen"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,28 +0,0 @@
|
||||
import '@app/styles.css';
|
||||
import { MuxPoster } from '@app/shared/react/mux-poster';
|
||||
import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
|
||||
import { VideoProvider } from '@app/shared/react/providers';
|
||||
import { VideoSkinComponent } from '@app/shared/react/skins';
|
||||
import { useSkin } from '@app/shared/react/use-skin';
|
||||
import { useSource } from '@app/shared/react/use-source';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import { Video } from '@videojs/react/video';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
function App() {
|
||||
const skin = useSkin();
|
||||
const source = useSource();
|
||||
|
||||
return (
|
||||
<VideoProvider>
|
||||
<VideoSkinComponent skin={skin} styling="tailwind" className="w-full aspect-video max-w-4xl mx-auto">
|
||||
<Video src={SOURCES[source].url} playsInline crossOrigin="anonymous">
|
||||
<MuxStoryboard source={source} />
|
||||
</Video>
|
||||
<MuxPoster source={source} />
|
||||
</VideoSkinComponent>
|
||||
</VideoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />);
|
||||
@@ -1,6 +1,4 @@
|
||||
import '@app/styles.css';
|
||||
import '@videojs/react/video/skin.css';
|
||||
import '@videojs/react/video/minimal-skin.css';
|
||||
import { MuxPoster } from '@app/shared/react/mux-poster';
|
||||
import { MuxStoryboard } from '@app/shared/react/mux-storyboard';
|
||||
import { VideoProvider } from '@app/shared/react/providers';
|
||||
@@ -8,16 +6,23 @@ import { VideoSkinComponent } from '@app/shared/react/skins';
|
||||
import { useSkin } from '@app/shared/react/use-skin';
|
||||
import { useSource } from '@app/shared/react/use-source';
|
||||
import { SOURCES } from '@app/shared/sources';
|
||||
import type { Styling } from '@app/types';
|
||||
import { Video } from '@videojs/react/video';
|
||||
import { useMemo } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
function readStyling(): Styling {
|
||||
return new URLSearchParams(location.search).get('styling') === 'tailwind' ? 'tailwind' : 'css';
|
||||
}
|
||||
|
||||
function App() {
|
||||
const skin = useSkin();
|
||||
const source = useSource();
|
||||
const styling = useMemo(readStyling, []);
|
||||
|
||||
return (
|
||||
<VideoProvider>
|
||||
<VideoSkinComponent skin={skin} styling="css" className="w-full aspect-video max-w-4xl mx-auto">
|
||||
<VideoSkinComponent skin={skin} styling={styling} className="w-full aspect-video max-w-4xl mx-auto">
|
||||
<Video src={SOURCES[source].url} playsInline crossOrigin="anonymous">
|
||||
<MuxStoryboard source={source} />
|
||||
</Video>
|
||||
|
||||
Reference in New Issue
Block a user