Bluesky app fork with some witchin' additions 💫

added video download button (#31)

authored by

m-doescode and committed by
GitHub
2a4eb5c9 a5822d83

+113
+22
src/lib/media/manip.ts
··· 21 21 import {logger} from '#/logger' 22 22 import {isAndroid, isIOS} from '#/platform/detection' 23 23 import {Dimensions} from './types' 24 + import {mimeToExt} from './video/util' 24 25 25 26 export async function compressIfNeeded( 26 27 img: Image, ··· 142 143 // save 143 144 await MediaLibrary.createAssetAsync(imagePath) 144 145 safeDeleteAsync(imagePath) 146 + } 147 + 148 + export async function saveVideoToMediaLibrary({uri}: {uri: string}) { 149 + // download the file to cache 150 + const downloadResponse = await RNFetchBlob.config({ 151 + fileCache: true, 152 + }) 153 + .fetch('GET', uri) 154 + .catch(() => null) 155 + if (downloadResponse == null) return false 156 + let videoPath = downloadResponse.path() 157 + let extension = mimeToExt(downloadResponse.respInfo.headers['content-type']) 158 + videoPath = normalizePath( 159 + await moveToPermanentPath(videoPath, '.' + extension), 160 + true, 161 + ) 162 + 163 + // save 164 + await MediaLibrary.createAssetAsync(videoPath) 165 + safeDeleteAsync(videoPath) 166 + return true 145 167 } 146 168 147 169 export function getImageDim(path: string): Promise<Dimensions> {
+17
src/lib/media/manip.web.ts
··· 2 2 3 3 import {Dimensions} from './types' 4 4 import {blobToDataUri, getDataUriSize} from './util' 5 + import {mimeToExt} from './video/util' 5 6 6 7 export async function compressIfNeeded( 7 8 img: RNImage, ··· 46 47 export async function saveImageToAlbum(_opts: {uri: string; album: string}) { 47 48 // TODO 48 49 throw new Error('TODO') 50 + } 51 + 52 + export async function downloadVideoWeb({uri}: {uri: string}) { 53 + // download the file to cache 54 + const downloadResponse = await fetch(uri) 55 + .then(res => res.blob()) 56 + .catch(() => null) 57 + if (downloadResponse == null) return false 58 + const extension = mimeToExt(downloadResponse.type) 59 + 60 + const blobUrl = URL.createObjectURL(downloadResponse) 61 + const link = document.createElement('a') 62 + link.setAttribute('download', uri.slice(-10) + '.' + extension) 63 + link.setAttribute('href', blobUrl) 64 + link.click() 65 + return true 49 66 } 50 67 51 68 export async function getImageDim(path: string): Promise<Dimensions> {
+74
src/view/com/util/forms/PostDropdownBtnMenuItems.tsx
··· 7 7 } from 'react-native' 8 8 import * as Clipboard from 'expo-clipboard' 9 9 import { 10 + type AppBskyEmbedExternal, 11 + type AppBskyEmbedVideo, 10 12 type AppBskyFeedDefs, 11 13 AppBskyFeedPost, 12 14 type AppBskyFeedThreadgate, ··· 18 20 import {useNavigation} from '@react-navigation/native' 19 21 20 22 import {useOpenLink} from '#/lib/hooks/useOpenLink' 23 + import {saveVideoToMediaLibrary} from '#/lib/media/manip' 24 + import {downloadVideoWeb} from '#/lib/media/manip.web' 21 25 import {getCurrentRoute} from '#/lib/routes/helpers' 22 26 import {makeProfileLink} from '#/lib/routes/links' 23 27 import { ··· 64 68 import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' 65 69 import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' 66 70 import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets' 71 + import {Download_Stroke2_Corner0_Rounded as Download} from '#/components/icons/Download' 67 72 import { 68 73 EmojiSad_Stroke2_Corner0_Rounded as EmojiSad, 69 74 EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile, ··· 386 391 }) 387 392 }, [isPinned, pinPostMutate, postCid, postUri]) 388 393 394 + const onPressDownloadVideo = useCallback(async () => { 395 + if (post.embed?.$type !== 'app.bsky.embed.video#view') return 396 + const video = post.embed as AppBskyEmbedVideo.View 397 + const did = post.author.did 398 + const cid = video.cid 399 + const uri = `https://bsky.social/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${cid}` 400 + 401 + Toast.show('Downloading video...', 'download') 402 + 403 + let success 404 + if (isWeb) success = await downloadVideoWeb({uri: uri}) 405 + else success = await saveVideoToMediaLibrary({uri: uri}) 406 + 407 + if (success) Toast.show('Video downloaded', 'check') 408 + else Toast.show('Failed to download video', 'xmark') 409 + }, [post]) 410 + 411 + const onPressDownloadGif = useCallback(async () => { 412 + if (post.embed?.$type !== 'app.bsky.embed.external#view') return 413 + const media = post.embed as AppBskyEmbedExternal.View 414 + 415 + Toast.show('Downloading GIF...', 'download') 416 + 417 + let success 418 + if (isWeb) success = await downloadVideoWeb({uri: media.external.uri}) 419 + else success = await saveVideoToMediaLibrary({uri: media.external.uri}) 420 + 421 + if (success) Toast.show('GIF downloaded', 'check') 422 + else Toast.show('Failed to download GIF', 'xmark') 423 + }, [post]) 424 + 425 + const isEmbedGif = useCallback(() => { 426 + if (post.embed?.$type !== 'app.bsky.embed.external#view') return false 427 + const embed = post.embed as AppBskyEmbedExternal.View 428 + // Janky workaround by checking if the domain is tenor.com 429 + const url = new URL(embed.external.uri) 430 + return url.host == 'media.tenor.com' 431 + }, [post]) 432 + 389 433 const onBlockAuthor = useCallback(async () => { 390 434 try { 391 435 await queueBlock() ··· 454 498 icon={isPinPending ? Loader : PinIcon} 455 499 position="right" 456 500 /> 501 + </Menu.Item> 502 + </Menu.Group> 503 + <Menu.Divider /> 504 + </> 505 + )} 506 + 507 + {post.embed?.$type === 'app.bsky.embed.video#view' && ( 508 + <> 509 + <Menu.Group> 510 + <Menu.Item 511 + testID="postDropdownDownloadVideoBtn" 512 + label={_(msg`Download Video`)} 513 + onPress={onPressDownloadVideo}> 514 + <Menu.ItemText>{_(msg`Download Video`)}</Menu.ItemText> 515 + <Menu.ItemIcon icon={Download} position="right" /> 516 + </Menu.Item> 517 + </Menu.Group> 518 + <Menu.Divider /> 519 + </> 520 + )} 521 + 522 + {isEmbedGif() && ( 523 + <> 524 + <Menu.Group> 525 + <Menu.Item 526 + testID="postDropdownDownloadGifBtn" 527 + label={_(msg`Download GIF`)} 528 + onPress={onPressDownloadGif}> 529 + <Menu.ItemText>{_(msg`Download GIF`)}</Menu.ItemText> 530 + <Menu.ItemIcon icon={Download} position="right" /> 457 531 </Menu.Item> 458 532 </Menu.Group> 459 533 <Menu.Divider />