import { View, StyleSheet, ScrollView } from 'react-native'; import { useBuffering, useControlsVisibility, useSettings, usePlaybackRate } from '../hooks'; import Animated, { useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated'; import { type FC } from 'react'; import { useVideo } from '../providers'; import { Menu, Subtitle, Title, CommonLayoutStyles as layoutStyles, VideoPlayer, type TitleProps, type SubitleProps, } from '../components'; export interface DefaultLayoutProps { title?: string; titleProps?: Omit; subtitle?: string; subtitleProps?: Omit; slots?: { beforeSettingsButton?: React.ReactElement; afterSettingsButton?: React.ReactElement; beforeSubtitleToggleButton?: React.ReactElement; afterSubtitleToggleButton?: React.ReactElement; /** * Here, before is above the progress bar */ beforeProgressBar?: React.ReactElement; /** * Here, after is below the progress bar */ afterProgressBar?: React.ReactElement; beforeTimeDisplay?: React.ReactElement; afterTimeDisplay?: React.ReactElement; beforeMuteButton?: React.ReactElement; afterMuteButton?: React.ReactElement; beforeFullscreenButton?: React.ReactElement; afterFullscreenButton?: React.ReactElement; beforeCenterPlayButton?: React.ReactElement; afterCenterPlayButton?: React.ReactElement; }; } /** * A predefined layout for the video player. * * @param {DefaultLayoutProps} props - The props for the component. * @returns {React.ReactElement} The default layout component. */ export const DefaultLayout: FC = ({ title, titleProps, subtitle, subtitleProps, slots, }: DefaultLayoutProps): React.ReactElement => { const { buffering } = useBuffering(); const { state } = useVideo(); const { videoTracks, textTracks, audioTracks, videoTrack, textTrack, audioTrack, setVideoTrack, setTextTrack, setAudioTrack, } = useSettings(); const { controlsVisible } = useControlsVisibility(); const { playbackRate, setPlaybackRate } = usePlaybackRate(); const topControlsAnimatedStyle = useAnimatedStyle(() => { return { opacity: withTiming(controlsVisible ? 1 : 0, { duration: 100, easing: Easing.bezierFn(0.25, 0.1, 0.25, 1) }), transform: [ { translateY: withTiming(controlsVisible ? 0 : -50, { duration: 100, easing: Easing.bezierFn(0.25, 0.1, 0.25, 1), }), }, ], }; }, [controlsVisible]); const bottomControlsAnimatedStyle = useAnimatedStyle(() => { return { opacity: withTiming(controlsVisible ? 1 : 0, { duration: 100, easing: Easing.bezierFn(0.25, 0.1, 0.25, 1) }), transform: [ { translateY: withTiming(controlsVisible ? 0 : 50, { duration: 100, easing: Easing.bezierFn(0.25, 0.1, 0.25, 1), }), }, ], }; }, [controlsVisible]); const centerControlsAnimatedStyle = useAnimatedStyle(() => { return { opacity: withTiming(controlsVisible || buffering ? 1 : 0, { duration: 100, easing: Easing.bezierFn(0.25, 0.1, 0.25, 1), }), // Keep position absolute and translate for centering }; }, [controlsVisible, buffering]); return ( <> {title && } {subtitle && <Subtitle text={subtitle} {...subtitleProps} />} </View> <View style={[layoutStyles.row]}> <View style={[layoutStyles.row]}> {slots?.beforeSubtitleToggleButton && slots.beforeSubtitleToggleButton} <VideoPlayer.SubtitleToggleButton /> {slots?.afterSubtitleToggleButton && slots.afterSubtitleToggleButton} </View> <View style={[layoutStyles.row]}> {slots?.beforeSettingsButton && slots.beforeSettingsButton} <Menu.Root> <Menu.Trigger /> <Menu.Content> <Menu.SubContent viewId="root"> <Menu.Item navigateTo="audio"> <View style={[layoutStyles.row, { justifyContent: 'space-between' }]}> <Title text="Audio" /> <Subtitle text={audioTrack ? (audioTrack.title! ?? audioTrack.language!) : 'None'} /> </View> </Menu.Item> <Menu.Item navigateTo="video"> <View style={[layoutStyles.row, { justifyContent: 'space-between' }]}> <Title text="Video" /> <Subtitle text={videoTrack ? ((videoTrack as any).label ?? videoTrack.height + 'p') : 'None'} /> </View> </Menu.Item> <Menu.Item navigateTo="captions"> <View style={[layoutStyles.row, { justifyContent: 'space-between' }]}> <Title text="Captions" /> <Subtitle text={textTrack ? (textTrack.title! ?? textTrack.language!) : 'Off'} /> </View> </Menu.Item> <Menu.Item navigateTo="playbackRate"> <View style={[layoutStyles.row, { justifyContent: 'space-between' }]}> <Title text="Rate" /> <Subtitle text={`${playbackRate}x`} /> </View> </Menu.Item> </Menu.SubContent> <Menu.SubContent viewId="audio"> {audioTracks.length > 0 ? ( <ScrollView style={{ maxHeight: '100%' }}> {audioTracks.map((track) => ( <Menu.CheckboxItem key={track.index} checked={audioTrack?.index === track.index} onCheckedChange={() => { setAudioTrack(track); }}> {track.title ?? track.language} </Menu.CheckboxItem> ))} </ScrollView> ) : ( <View style={{ padding: 10 }}> <Subtitle text="No audio tracks available" /> </View> )} </Menu.SubContent> <Menu.SubContent viewId="video"> {videoTracks.length > 0 ? ( <ScrollView style={{ maxHeight: '100%' }}> {videoTracks.map((track) => ( <Menu.CheckboxItem key={track.index} checked={videoTrack?.index === track.index} onCheckedChange={() => { setVideoTrack(track); }}> {(track as any).label ?? track.height + 'p'} </Menu.CheckboxItem> ))} </ScrollView> ) : ( <View style={{ padding: 10 }}> <Subtitle text="No video tracks available" /> </View> )} </Menu.SubContent> <Menu.SubContent viewId="captions"> {textTracks.length > 0 ? ( <ScrollView style={{ maxHeight: '100%' }}> {textTracks.map((track) => ( <Menu.CheckboxItem key={track.index} checked={textTrack?.index === track.index} onCheckedChange={() => { setTextTrack(track); }}> {track.title ?? track.language} </Menu.CheckboxItem> ))} </ScrollView> ) : ( <View style={{ padding: 10 }}> <Subtitle text="No captions available" /> </View> )} </Menu.SubContent> <Menu.SubContent viewId="playbackRate"> <ScrollView style={{ maxHeight: '100%' }}> {state.config.playbackRates.map((rate) => ( <Menu.CheckboxItem key={rate} checked={playbackRate === rate} onCheckedChange={(checked) => { if (checked) { setPlaybackRate(rate); } }}> {rate}x </Menu.CheckboxItem> ))} </ScrollView> </Menu.SubContent> </Menu.Content> </Menu.Root> {slots?.afterSettingsButton && slots.afterSettingsButton} </View> </View> </Animated.View> <Animated.View style={[centerControlsAnimatedStyle, layoutStyles.centerControls]}> {!buffering ? ( <View style={layoutStyles.row}> {slots?.beforeCenterPlayButton && slots.beforeCenterPlayButton} <VideoPlayer.PlayButton size={state.theme.iconSizes.lg} /> {slots?.afterCenterPlayButton && slots.afterCenterPlayButton} </View> ) : null} </Animated.View> <Animated.View style={[ layoutStyles.bottomControls, bottomControlsAnimatedStyle, { padding: state.fullscreen ? 20 : 10, }, ]}> <View style={[layoutStyles.column]}> {slots?.beforeProgressBar && slots.beforeProgressBar} <VideoPlayer.ProgressBar /> {slots?.afterProgressBar && slots.afterProgressBar} </View> <View style={layoutStyles.row}> <View style={layoutStyles.row}> {slots?.beforeTimeDisplay && slots.beforeTimeDisplay} <VideoPlayer.TimeDisplay /> {slots?.afterTimeDisplay && slots.afterTimeDisplay} </View> <View style={layoutStyles.spacer} /> <View style={[layoutStyles.row]}> <View style={[layoutStyles.row]}> {slots?.beforeFullscreenButton && slots.beforeFullscreenButton} <VideoPlayer.FullscreenButton /> {slots?.afterFullscreenButton && slots.afterFullscreenButton} </View> <View style={[layoutStyles.row]}> {slots?.beforeMuteButton && slots.beforeMuteButton} <VideoPlayer.MuteButton /> {slots?.afterMuteButton && slots.afterMuteButton} </View> </View> </View> </Animated.View> </View> </VideoPlayer.Controls> </Animated.View> {/* Separate loading spinner that's always visible when buffering */} {buffering && ( <View style={[styles.baseStyle, { pointerEvents: 'none' }]}> <VideoPlayer.LoadingSpinner /> </View> )} </> ); }; const styles = StyleSheet.create({ baseStyle: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'space-between', }, });