Bluesky app fork with some witchin' additions 💫

[Video] Captions and alt text (#5009)

* video settings modal in composer

* show done button on web

* rm download options

* fix logic for showing settings button

* add language picker (wip)

* subtitle list with language select

* send captions & alt text with video when posting

* style "ensure you have selected a language" text

* include aspect ratio with video

* filter out captions where the lang is not set

* rm log

* fix label and add hint

* minor scrubber fix

authored by samuel.fm and committed by

GitHub c70ec1ce e7954e59

+503 -30
+9
src/alf/atoms.ts
··· 853 853 mr_auto: { 854 854 marginRight: 'auto', 855 855 }, 856 + 856 857 /* 857 858 * Pointer events & user select 858 859 */ ··· 871 872 user_select_all: { 872 873 userSelect: 'all', 873 874 }, 875 + 874 876 /* 875 877 * Text decoration 876 878 */ ··· 879 881 }, 880 882 strike_through: { 881 883 textDecorationLine: 'line-through', 884 + }, 885 + 886 + /* 887 + * Display 888 + */ 889 + hidden: { 890 + display: 'none', 882 891 }, 883 892 } as const
+25 -3
src/lib/api/index.ts
··· 1 1 import { 2 + AppBskyEmbedDefs, 2 3 AppBskyEmbedExternal, 3 4 AppBskyEmbedImages, 4 5 AppBskyEmbedRecord, ··· 45 46 uri: string 46 47 cid: string 47 48 } 48 - video?: BlobRef 49 + video?: { 50 + blobRef: BlobRef 51 + altText: string 52 + captions: {lang: string; file: File}[] 53 + aspectRatio?: AppBskyEmbedDefs.AspectRatio 54 + } 49 55 extLink?: ExternalEmbedDraft 50 56 images?: ImageModel[] 51 57 labels?: string[] ··· 128 134 129 135 // add video embed if present 130 136 if (opts.video) { 137 + const captions = await Promise.all( 138 + opts.video.captions 139 + .filter(caption => caption.lang !== '') 140 + .map(async caption => { 141 + const {data} = await agent.uploadBlob(caption.file, { 142 + encoding: 'text/vtt', 143 + }) 144 + return {lang: caption.lang, file: data.blob} 145 + }), 146 + ) 131 147 if (opts.quote) { 132 148 embed = { 133 149 $type: 'app.bsky.embed.recordWithMedia', 134 150 record: embed, 135 151 media: { 136 152 $type: 'app.bsky.embed.video', 137 - video: opts.video, 153 + video: opts.video.blobRef, 154 + alt: opts.video.altText || undefined, 155 + captions: captions.length === 0 ? undefined : captions, 156 + aspectRatio: opts.video.aspectRatio, 138 157 } as AppBskyEmbedVideo.Main, 139 158 } as AppBskyEmbedRecordWithMedia.Main 140 159 } else { 141 160 embed = { 142 161 $type: 'app.bsky.embed.video', 143 - video: opts.video, 162 + video: opts.video.blobRef, 163 + alt: opts.video.altText || undefined, 164 + captions: captions.length === 0 ? undefined : captions, 165 + aspectRatio: opts.video.aspectRatio, 144 166 } as AppBskyEmbedVideo.Main 145 167 } 146 168 }
+3 -3
src/lib/moderation/useLabelInfo.ts
··· 1 1 import { 2 - ComAtprotoLabelDefs, 3 2 AppBskyLabelerDefs, 4 - LABELS, 5 - interpretLabelValueDefinition, 3 + ComAtprotoLabelDefs, 6 4 InterpretedLabelValueDefinition, 5 + interpretLabelValueDefinition, 6 + LABELS, 7 7 } from '@atproto/api' 8 8 import {useLingui} from '@lingui/react' 9 9 import * as bcp47Match from 'bcp-47-match'
+18
src/lib/strings/helpers.ts
··· 1 + import {useCallback, useMemo} from 'react' 2 + import Graphemer from 'graphemer' 3 + 1 4 export function enforceLen( 2 5 str: string, 3 6 len: number, ··· 21 24 } 22 25 } 23 26 return str 27 + } 28 + 29 + export function useEnforceMaxGraphemeCount() { 30 + const splitter = useMemo(() => new Graphemer(), []) 31 + 32 + return useCallback( 33 + (text: string, maxCount: number) => { 34 + if (splitter.countGraphemes(text) > maxCount) { 35 + return splitter.splitGraphemes(text).slice(0, maxCount).join('') 36 + } else { 37 + return text 38 + } 39 + }, 40 + [splitter], 41 + ) 24 42 } 25 43 26 44 // https://stackoverflow.com/a/52171480
+18 -1
src/state/queries/video/video.ts
··· 1 - import React from 'react' 1 + import React, {useCallback} from 'react' 2 2 import {ImagePickerAsset} from 'expo-image-picker' 3 3 import {AppBskyVideoDefs, BlobRef} from '@atproto/api' 4 4 import {msg} from '@lingui/macro' ··· 20 20 | {type: 'SetError'; error: string | undefined} 21 21 | {type: 'Reset'} 22 22 | {type: 'SetAsset'; asset: ImagePickerAsset} 23 + | {type: 'SetDimensions'; width: number; height: number} 23 24 | {type: 'SetVideo'; video: CompressedVideo} 24 25 | {type: 'SetJobStatus'; jobStatus: AppBskyVideoDefs.JobStatus} 25 26 | {type: 'SetBlobRef'; blobRef: BlobRef} ··· 58 59 } 59 60 } else if (action.type === 'SetAsset') { 60 61 updatedState = {...state, asset: action.asset} 62 + } else if (action.type === 'SetDimensions') { 63 + updatedState = { 64 + ...state, 65 + asset: state.asset 66 + ? {...state.asset, width: action.width, height: action.height} 67 + : undefined, 68 + } 61 69 } else if (action.type === 'SetVideo') { 62 70 updatedState = {...state, video: action.video} 63 71 } else if (action.type === 'SetJobStatus') { ··· 178 186 dispatch({type: 'Reset'}) 179 187 } 180 188 189 + const updateVideoDimensions = useCallback((width: number, height: number) => { 190 + dispatch({ 191 + type: 'SetDimensions', 192 + width, 193 + height, 194 + }) 195 + }, []) 196 + 181 197 return { 182 198 state, 183 199 dispatch, 184 200 selectVideo, 185 201 clearVideo, 202 + updateVideoDimensions, 186 203 } 187 204 } 188 205
+40 -10
src/view/com/composer/Composer.tsx
··· 108 108 import {ThreadgateBtn} from './threadgate/ThreadgateBtn' 109 109 import {useExternalLinkFetch} from './useExternalLinkFetch' 110 110 import {SelectVideoBtn} from './videos/SelectVideoBtn' 111 + import {SubtitleDialogBtn} from './videos/SubtitleDialog' 111 112 import {VideoPreview} from './videos/VideoPreview' 112 113 import {VideoTranscodeProgress} from './videos/VideoTranscodeProgress' 113 114 ··· 172 173 initQuote, 173 174 ) 174 175 176 + const [videoAltText, setVideoAltText] = useState('') 177 + const [captions, setCaptions] = useState<{lang: string; file: File}[]>([]) 178 + 175 179 const { 176 180 selectVideo, 177 181 clearVideo, 178 182 state: videoUploadState, 183 + updateVideoDimensions, 179 184 } = useUploadVideo({ 180 185 setStatus: setProcessingState, 181 186 onSuccess: () => { ··· 347 352 postgate, 348 353 onStateChange: setProcessingState, 349 354 langs: toPostLanguages(langPrefs.postLanguage), 350 - video: videoUploadState.blobRef, 355 + video: videoUploadState.blobRef 356 + ? { 357 + blobRef: videoUploadState.blobRef, 358 + altText: videoAltText, 359 + captions: captions, 360 + aspectRatio: videoUploadState.asset 361 + ? { 362 + width: videoUploadState.asset?.width, 363 + height: videoUploadState.asset?.height, 364 + } 365 + : undefined, 366 + } 367 + : undefined, 351 368 }) 352 369 ).uri 353 370 try { ··· 694 711 )} 695 712 </View> 696 713 ) : null} 697 - {videoUploadState.status === 'compressing' && 698 - videoUploadState.asset ? ( 699 - <VideoTranscodeProgress 700 - asset={videoUploadState.asset} 701 - progress={videoUploadState.progress} 702 - clear={clearVideo} 714 + {videoUploadState.asset && 715 + (videoUploadState.status === 'compressing' ? ( 716 + <VideoTranscodeProgress 717 + asset={videoUploadState.asset} 718 + progress={videoUploadState.progress} 719 + clear={clearVideo} 720 + /> 721 + ) : videoUploadState.video ? ( 722 + <VideoPreview 723 + asset={videoUploadState.asset} 724 + video={videoUploadState.video} 725 + setDimensions={updateVideoDimensions} 726 + clear={clearVideo} 727 + /> 728 + ) : null)} 729 + {(videoUploadState.asset || videoUploadState.video) && ( 730 + <SubtitleDialogBtn 731 + altText={videoAltText} 732 + setAltText={setVideoAltText} 733 + captions={captions} 734 + setCaptions={setCaptions} 703 735 /> 704 - ) : videoUploadState.video ? ( 705 - <VideoPreview video={videoUploadState.video} clear={clearVideo} /> 706 - ) : null} 736 + )} 707 737 </View> 708 738 </Animated.ScrollView> 709 739 <SuggestedLanguage text={richtext.text} />
+265
src/view/com/composer/videos/SubtitleDialog.tsx
··· 1 + import React, {useCallback} from 'react' 2 + import {StyleProp, View, ViewStyle} from 'react-native' 3 + import RNPickerSelect from 'react-native-picker-select' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + 7 + import {MAX_ALT_TEXT} from '#/lib/constants' 8 + import {useEnforceMaxGraphemeCount} from '#/lib/strings/helpers' 9 + import {LANGUAGES} from '#/locale/languages' 10 + import {isWeb} from '#/platform/detection' 11 + import {useLanguagePrefs} from '#/state/preferences' 12 + import {atoms as a, useTheme, web} from '#/alf' 13 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 14 + import * as Dialog from '#/components/Dialog' 15 + import * as TextField from '#/components/forms/TextField' 16 + import {CC_Stroke2_Corner0_Rounded as CCIcon} from '#/components/icons/CC' 17 + import {PageText_Stroke2_Corner0_Rounded as PageTextIcon} from '#/components/icons/PageText' 18 + import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 19 + import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 20 + import {Text} from '#/components/Typography' 21 + import {SubtitleFilePicker} from './SubtitleFilePicker' 22 + 23 + interface Props { 24 + altText: string 25 + captions: {lang: string; file: File}[] 26 + setAltText: (altText: string) => void 27 + setCaptions: React.Dispatch< 28 + React.SetStateAction<{lang: string; file: File}[]> 29 + > 30 + } 31 + 32 + export function SubtitleDialogBtn(props: Props) { 33 + const control = Dialog.useDialogControl() 34 + const {_} = useLingui() 35 + 36 + return ( 37 + <View style={[a.flex_row, a.mt_xs]}> 38 + <Button 39 + label={isWeb ? _('Captions & alt text') : _('Alt text')} 40 + accessibilityHint={ 41 + isWeb 42 + ? _('Opens captions and alt text dialog') 43 + : _('Opens alt text dialog') 44 + } 45 + size="xsmall" 46 + color="secondary" 47 + variant="ghost" 48 + onPress={control.open}> 49 + <ButtonIcon icon={CCIcon} /> 50 + <ButtonText> 51 + {isWeb ? <Trans>Captions & alt text</Trans> : <Trans>Alt text</Trans>} 52 + </ButtonText> 53 + </Button> 54 + <Dialog.Outer control={control}> 55 + <Dialog.Handle /> 56 + <SubtitleDialogInner {...props} /> 57 + </Dialog.Outer> 58 + </View> 59 + ) 60 + } 61 + 62 + function SubtitleDialogInner({ 63 + altText, 64 + setAltText, 65 + captions, 66 + setCaptions, 67 + }: Props) { 68 + const control = Dialog.useDialogContext() 69 + const {_} = useLingui() 70 + const t = useTheme() 71 + const enforceLen = useEnforceMaxGraphemeCount() 72 + const {primaryLanguage} = useLanguagePrefs() 73 + 74 + const handleSelectFile = useCallback( 75 + (file: File) => { 76 + setCaptions(subs => [ 77 + ...subs, 78 + { 79 + lang: subs.some(s => s.lang === primaryLanguage) 80 + ? '' 81 + : primaryLanguage, 82 + file, 83 + }, 84 + ]) 85 + }, 86 + [setCaptions, primaryLanguage], 87 + ) 88 + 89 + const subtitleMissingLanguage = captions.some(sub => sub.lang === '') 90 + 91 + return ( 92 + <Dialog.ScrollableInner label={_(msg`Video settings`)}> 93 + <View style={a.gap_md}> 94 + <Text style={[a.text_xl, a.font_bold, a.leading_tight]}> 95 + <Trans>Alt text</Trans> 96 + </Text> 97 + <TextField.Root> 98 + <Dialog.Input 99 + label={_(msg`Alt text`)} 100 + placeholder={_(msg`Add alt text (optional)`)} 101 + value={altText} 102 + onChangeText={evt => setAltText(enforceLen(evt, MAX_ALT_TEXT))} 103 + maxLength={MAX_ALT_TEXT * 10} 104 + multiline 105 + numberOfLines={3} 106 + onKeyPress={({nativeEvent}) => { 107 + if (nativeEvent.key === 'Escape') { 108 + control.close() 109 + } 110 + }} 111 + /> 112 + </TextField.Root> 113 + 114 + {isWeb && ( 115 + <> 116 + <View 117 + style={[ 118 + a.border_t, 119 + a.w_full, 120 + t.atoms.border_contrast_medium, 121 + a.my_md, 122 + ]} 123 + /> 124 + <Text style={[a.text_xl, a.font_bold, a.leading_tight]}> 125 + <Trans>Captions (.vtt)</Trans> 126 + </Text> 127 + <SubtitleFilePicker 128 + onSelectFile={handleSelectFile} 129 + disabled={subtitleMissingLanguage || captions.length >= 4} 130 + /> 131 + <View> 132 + {captions.map((subtitle, i) => ( 133 + <SubtitleFileRow 134 + key={subtitle.lang} 135 + language={subtitle.lang} 136 + file={subtitle.file} 137 + setCaptions={setCaptions} 138 + otherLanguages={LANGUAGES.filter( 139 + lang => 140 + langCode(lang) === subtitle.lang || 141 + !captions.some(s => s.lang === langCode(lang)), 142 + )} 143 + style={[i % 2 === 0 && t.atoms.bg_contrast_25]} 144 + /> 145 + ))} 146 + </View> 147 + </> 148 + )} 149 + 150 + {subtitleMissingLanguage && ( 151 + <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 152 + Ensure you have selected a language for each subtitle file. 153 + </Text> 154 + )} 155 + 156 + <View style={web([a.flex_row, a.justify_end])}> 157 + <Button 158 + label={_(msg`Done`)} 159 + size={isWeb ? 'small' : 'medium'} 160 + color="primary" 161 + variant="solid" 162 + onPress={() => control.close()} 163 + style={a.mt_lg}> 164 + <ButtonText> 165 + <Trans>Done</Trans> 166 + </ButtonText> 167 + </Button> 168 + </View> 169 + </View> 170 + <Dialog.Close /> 171 + </Dialog.ScrollableInner> 172 + ) 173 + } 174 + 175 + function SubtitleFileRow({ 176 + language, 177 + file, 178 + otherLanguages, 179 + setCaptions, 180 + style, 181 + }: { 182 + language: string 183 + file: File 184 + otherLanguages: {code2: string; code3: string; name: string}[] 185 + setCaptions: React.Dispatch< 186 + React.SetStateAction<{lang: string; file: File}[]> 187 + > 188 + style: StyleProp<ViewStyle> 189 + }) { 190 + const {_} = useLingui() 191 + const t = useTheme() 192 + 193 + const handleValueChange = useCallback( 194 + (lang: string) => { 195 + if (lang) { 196 + setCaptions(subs => 197 + subs.map(s => (s.lang === language ? {lang, file: s.file} : s)), 198 + ) 199 + } 200 + }, 201 + [setCaptions, language], 202 + ) 203 + 204 + return ( 205 + <View 206 + style={[ 207 + a.flex_row, 208 + a.justify_between, 209 + a.py_md, 210 + a.px_lg, 211 + a.rounded_md, 212 + a.gap_md, 213 + style, 214 + ]}> 215 + <View style={[a.flex_1, a.gap_xs, a.justify_center]}> 216 + <View style={[a.flex_row, a.align_center, a.gap_sm]}> 217 + {language === '' ? ( 218 + <WarningIcon 219 + style={a.flex_shrink_0} 220 + fill={t.palette.negative_500} 221 + size="sm" 222 + /> 223 + ) : ( 224 + <PageTextIcon style={[t.atoms.text, a.flex_shrink_0]} size="sm" /> 225 + )} 226 + <Text 227 + style={[a.flex_1, a.leading_snug, a.font_bold, a.mb_2xs]} 228 + numberOfLines={1}> 229 + {file.name} 230 + </Text> 231 + <RNPickerSelect 232 + placeholder={{ 233 + label: _(msg`Select language...`), 234 + value: '', 235 + }} 236 + value={language} 237 + onValueChange={handleValueChange} 238 + items={otherLanguages.map(lang => ({ 239 + label: `${lang.name} (${langCode(lang)})`, 240 + value: langCode(lang), 241 + }))} 242 + style={{viewContainer: {maxWidth: 200, flex: 1}}} 243 + /> 244 + </View> 245 + </View> 246 + 247 + <Button 248 + label={_(msg`Remove subtitle file`)} 249 + size="tiny" 250 + shape="round" 251 + variant="outline" 252 + color="secondary" 253 + onPress={() => 254 + setCaptions(subs => subs.filter(s => s.lang !== language)) 255 + } 256 + style={[a.ml_sm]}> 257 + <ButtonIcon icon={X} /> 258 + </Button> 259 + </View> 260 + ) 261 + } 262 + 263 + function langCode(lang: {code2: string; code3: string}) { 264 + return lang.code2 || lang.code3 265 + }
+3
src/view/com/composer/videos/SubtitleFilePicker.native.tsx
··· 1 + export function SubtitleFilePicker() { 2 + throw new Error('SubtitleFilePicker is a web-only component') 3 + }
+63
src/view/com/composer/videos/SubtitleFilePicker.tsx
··· 1 + import React, {useRef} from 'react' 2 + import {View} from 'react-native' 3 + import {msg, Trans} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import * as Toast from '#/view/com/util/Toast' 7 + import {atoms as a} from '#/alf' 8 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 9 + import {CC_Stroke2_Corner0_Rounded as CCIcon} from '#/components/icons/CC' 10 + 11 + export function SubtitleFilePicker({ 12 + onSelectFile, 13 + disabled, 14 + }: { 15 + onSelectFile: (file: File) => void 16 + disabled?: boolean 17 + }) { 18 + const {_} = useLingui() 19 + const ref = useRef<HTMLInputElement>(null) 20 + 21 + const handleClick = () => { 22 + ref.current?.click() 23 + } 24 + 25 + const handlePick = (evt: React.ChangeEvent<HTMLInputElement>) => { 26 + const selectedFile = evt.target.files?.[0] 27 + if (selectedFile) { 28 + if (selectedFile.type === 'text/vtt') { 29 + onSelectFile(selectedFile) 30 + } else { 31 + Toast.show(_(msg`Only WebVTT (.vtt) files are supported`)) 32 + } 33 + } 34 + } 35 + 36 + return ( 37 + <View style={a.gap_lg}> 38 + <input 39 + type="file" 40 + accept=".vtt" 41 + ref={ref} 42 + style={a.hidden} 43 + onChange={handlePick} 44 + disabled={disabled} 45 + aria-disabled={disabled} 46 + /> 47 + <View style={a.flex_row}> 48 + <Button 49 + onPress={handleClick} 50 + label={_('Select subtitle file (.vtt)')} 51 + size="medium" 52 + color="primary" 53 + variant="solid" 54 + disabled={disabled}> 55 + <ButtonIcon icon={CCIcon} /> 56 + <ButtonText> 57 + <Trans>Select subtitle file (.vtt)</Trans> 58 + </ButtonText> 59 + </Button> 60 + </View> 61 + </View> 62 + ) 63 + }
+11 -2
src/view/com/composer/videos/VideoPreview.tsx
··· 1 1 /* eslint-disable @typescript-eslint/no-shadow */ 2 2 import React from 'react' 3 3 import {View} from 'react-native' 4 + import {ImagePickerAsset} from 'expo-image-picker' 4 5 import {useVideoPlayer, VideoView} from 'expo-video' 5 6 6 7 import {CompressedVideo} from '#/lib/media/video/compress' 7 8 import {ExternalEmbedRemoveBtn} from 'view/com/composer/ExternalEmbedRemoveBtn' 8 - import {atoms as a} from '#/alf' 9 + import {atoms as a, useTheme} from '#/alf' 9 10 10 11 export function VideoPreview({ 12 + asset, 11 13 video, 12 14 clear, 13 15 }: { 16 + asset: ImagePickerAsset 14 17 video: CompressedVideo 18 + setDimensions: (width: number, height: number) => void 15 19 clear: () => void 16 20 }) { 21 + const t = useTheme() 17 22 const player = useVideoPlayer(video.uri, player => { 18 23 player.loop = true 19 24 player.muted = true 20 25 player.play() 21 26 }) 22 27 28 + const aspectRatio = asset.width / asset.height 29 + 23 30 return ( 24 31 <View 25 32 style={[ 26 33 a.w_full, 27 34 a.rounded_sm, 28 - {aspectRatio: 16 / 9}, 35 + {aspectRatio: isNaN(aspectRatio) ? 16 / 9 : aspectRatio}, 29 36 a.overflow_hidden, 37 + a.border, 38 + t.atoms.border_contrast_low, 30 39 ]}> 31 40 <VideoView 32 41 player={player}
+42 -4
src/view/com/composer/videos/VideoPreview.web.tsx
··· 1 - import React from 'react' 1 + import React, {useEffect, useRef} from 'react' 2 2 import {View} from 'react-native' 3 + import {ImagePickerAsset} from 'expo-image-picker' 3 4 4 5 import {CompressedVideo} from '#/lib/media/video/compress' 5 6 import {ExternalEmbedRemoveBtn} from 'view/com/composer/ExternalEmbedRemoveBtn' 6 - import {atoms as a} from '#/alf' 7 + import {atoms as a, useTheme} from '#/alf' 7 8 8 9 export function VideoPreview({ 10 + asset, 9 11 video, 12 + setDimensions, 10 13 clear, 11 14 }: { 15 + asset: ImagePickerAsset 12 16 video: CompressedVideo 17 + setDimensions: (width: number, height: number) => void 13 18 clear: () => void 14 19 }) { 20 + const t = useTheme() 21 + const ref = useRef<HTMLVideoElement>(null) 22 + 23 + useEffect(() => { 24 + if (!ref.current) return 25 + 26 + const abortController = new AbortController() 27 + const {signal} = abortController 28 + ref.current.addEventListener( 29 + 'loadedmetadata', 30 + function () { 31 + setDimensions(this.videoWidth, this.videoHeight) 32 + }, 33 + {signal}, 34 + ) 35 + 36 + return () => { 37 + abortController.abort() 38 + } 39 + }, [setDimensions]) 40 + 41 + const aspectRatio = asset.width / asset.height 42 + 15 43 return ( 16 44 <View 17 45 style={[ 18 46 a.w_full, 19 47 a.rounded_sm, 20 - {aspectRatio: 16 / 9}, 48 + 49 + {aspectRatio: isNaN(aspectRatio) ? 16 / 9 : aspectRatio}, 21 50 a.overflow_hidden, 51 + {backgroundColor: t.palette.black}, 22 52 ]}> 23 53 <ExternalEmbedRemoveBtn onRemove={clear} /> 24 - <video src={video.uri} style={a.flex_1} autoPlay loop muted playsInline /> 54 + <video 55 + ref={ref} 56 + src={video.uri} 57 + style={a.flex_1} 58 + autoPlay 59 + loop 60 + muted 61 + playsInline 62 + /> 25 63 </View> 26 64 ) 27 65 }
+2 -6
src/view/com/composer/videos/VideoTranscodeBackdrop.web.tsx
··· 1 - import React from 'react' 2 - 3 - export function VideoTranscodeBackdrop({uri}: {uri: string}) { 4 - return ( 5 - <video src={uri} style={{flex: 1, filter: 'blur(10px)'}} muted autoPlay /> 6 - ) 1 + export function VideoTranscodeBackdrop() { 2 + return null 7 3 }
+3
src/view/com/composer/videos/VideoTranscodeProgress.tsx
··· 4 4 import ProgressPie from 'react-native-progress/Pie' 5 5 import {ImagePickerAsset} from 'expo-image-picker' 6 6 7 + import {isWeb} from '#/platform/detection' 7 8 import {atoms as a, useTheme} from '#/alf' 8 9 import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn' 9 10 import {VideoTranscodeBackdrop} from './VideoTranscodeBackdrop' ··· 20 21 const t = useTheme() 21 22 22 23 const aspectRatio = asset.width / asset.height 24 + 25 + if (isWeb) return null 23 26 24 27 return ( 25 28 <View
+1 -1
src/view/com/util/post-embeds/VideoEmbedInner/VideoWebControls.tsx
··· 557 557 {backgroundColor: 'rgba(255, 255, 255, 0.4)'}, 558 558 {height: hovered || scrubberActive ? 6 : 3}, 559 559 ]}> 560 - {currentTime > 0 && duration > 0 && ( 560 + {duration > 0 && ( 561 561 <View 562 562 style={[ 563 563 a.h_full,