video player gif presentation style

This commit is contained in:
Samuel Newman
2026-01-30 17:28:42 +02:00
parent 812ff4d154
commit cd5cd02535
5 changed files with 127 additions and 14 deletions
@@ -0,0 +1,85 @@
import {Pressable, StyleSheet, View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Fill} from '#/components/Fill'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {PlayButtonIcon} from '#/components/video/PlayButtonIcon'
export function GifPresentationControls({
onPress,
isPlaying,
isLoading,
}: {
onPress: () => void
isPlaying: boolean
isLoading?: boolean
}) {
const {_} = useLingui()
const t = useTheme()
return (
<>
<Pressable
accessibilityRole="button"
accessibilityHint={_(msg`Plays or pauses the GIF`)}
accessibilityLabel={isPlaying ? _(msg`Pause`) : _(msg`Play`)}
style={[
a.absolute,
a.align_center,
a.justify_center,
a.inset_0,
a.w_full,
a.h_full,
{
zIndex: 2,
},
]}
onPress={onPress}>
{isLoading ? (
<View style={[a.align_center, a.justify_center]}>
<Loader size="xl" />
</View>
) : !isPlaying ? (
<PlayButtonIcon />
) : undefined}
</Pressable>
{!isPlaying && (
<Fill
style={[
t.name === 'light' ? t.atoms.bg_contrast_975 : t.atoms.bg,
{
opacity: 0.3,
zIndex: 1,
},
]}
/>
)}
<View style={styles.gifBadgeContainer}>
<Text style={styles.gifBadge}>
<Trans>GIF</Trans>
</Text>
</View>
</>
)
}
const styles = StyleSheet.create({
gifBadgeContainer: {
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6,
paddingHorizontal: 6,
paddingVertical: 3,
position: 'absolute',
right: 5,
bottom: 5,
zIndex: 2,
},
gifBadge: {
color: 'white',
fontSize: 7,
fontWeight: '600',
},
})
@@ -15,6 +15,7 @@ import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext' import {useVideoMuteState} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import {GifPresentationControls} from '../GifPresentationControls'
import {TimeIndicator} from './TimeIndicator' import {TimeIndicator} from './TimeIndicator'
export function VideoEmbedInnerNative({ export function VideoEmbedInnerNative({
@@ -50,6 +51,8 @@ export function VideoEmbedInnerNative({
throw new Error(error) throw new Error(error)
} }
const isGif = embed.presentation === 'gif'
return ( return (
<View style={[a.flex_1, a.relative]}> <View style={[a.flex_1, a.relative]}>
<BlueskyVideoView <BlueskyVideoView
@@ -82,25 +85,35 @@ export function VideoEmbedInnerNative({
} }
accessibilityHint="" accessibilityHint=""
/> />
<VideoControls {isGif ? (
enterFullscreen={() => { <GifPresentationControls
videoRef.current?.enterFullscreen(true) onPress={() => {
}} videoRef.current?.togglePlayback()
toggleMuted={() => { }}
videoRef.current?.toggleMuted() isPlaying={isPlaying}
}} isLoading={false}
togglePlayback={() => { />
videoRef.current?.togglePlayback() ) : (
}} <VideoPresentationControls
isPlaying={isPlaying} enterFullscreen={() => {
timeRemaining={timeRemaining} videoRef.current?.enterFullscreen(true)
/> }}
toggleMuted={() => {
videoRef.current?.toggleMuted()
}}
togglePlayback={() => {
videoRef.current?.togglePlayback()
}}
isPlaying={isPlaying}
timeRemaining={timeRemaining}
/>
)}
<MediaInsetBorder /> <MediaInsetBorder />
</View> </View>
) )
} }
function VideoControls({ function VideoPresentationControls({
enterFullscreen, enterFullscreen,
toggleMuted, toggleMuted,
togglePlayback, togglePlayback,
@@ -99,6 +99,7 @@ export function VideoEmbedInnerWeb({
onScreen={onScreen} onScreen={onScreen}
fullscreenRef={containerRef} fullscreenRef={containerRef}
hasSubtitleTrack={hasSubtitleTrack} hasSubtitleTrack={hasSubtitleTrack}
isGif={embed.presentation === 'gif'}
/> />
</div> </div>
</View> </View>
@@ -27,6 +27,7 @@ import {Play_Filled_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_WEB_MOBILE_IOS, IS_WEB_TOUCH_DEVICE} from '#/env' import {IS_WEB_MOBILE_IOS, IS_WEB_TOUCH_DEVICE} from '#/env'
import {GifPresentationControls} from '../../GifPresentationControls'
import {TimeIndicator} from '../TimeIndicator' import {TimeIndicator} from '../TimeIndicator'
import {ControlButton} from './ControlButton' import {ControlButton} from './ControlButton'
import {Scrubber} from './Scrubber' import {Scrubber} from './Scrubber'
@@ -44,6 +45,7 @@ export function Controls({
fullscreenRef, fullscreenRef,
hlsLoading, hlsLoading,
hasSubtitleTrack, hasSubtitleTrack,
isGif,
}: { }: {
videoRef: React.RefObject<HTMLVideoElement | null> videoRef: React.RefObject<HTMLVideoElement | null>
hlsRef: React.RefObject<Hls | undefined | null> hlsRef: React.RefObject<Hls | undefined | null>
@@ -55,6 +57,7 @@ export function Controls({
fullscreenRef: React.RefObject<HTMLDivElement | null> fullscreenRef: React.RefObject<HTMLDivElement | null>
hlsLoading: boolean hlsLoading: boolean
hasSubtitleTrack: boolean hasSubtitleTrack: boolean
isGif: boolean
}) { }) {
const { const {
play, play,
@@ -287,6 +290,16 @@ export function Controls({
((focused || autoplayDisabled) && !playing) || ((focused || autoplayDisabled) && !playing) ||
(interactingViaKeypress ? hasFocus : hovered) (interactingViaKeypress ? hasFocus : hovered)
if (isGif) {
return (
<GifPresentationControls
isPlaying={playing}
isLoading={showSpinner}
onPress={onPressPlayPause}
/>
)
}
return ( return (
<div <div
style={{ style={{
@@ -98,6 +98,7 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
setActive={setActive} setActive={setActive}
onScreen={onScreen} onScreen={onScreen}
lastKnownTime={lastKnownTime} lastKnownTime={lastKnownTime}
isGif={embed.presentation === 'gif'}
/> />
</OnlyNearScreen> </OnlyNearScreen>
</ErrorBoundary> </ErrorBoundary>