From c72e24f841b5771d74e82114e40ed81f15ec97a7 Mon Sep 17 00:00:00 2001 From: Paul Frazee <pfrazee@gmail.com> Date: Fri, 30 Jun 2023 11:34:04 -0500 Subject: [PATCH] [APP-716] Add 'save image' button to the lightbox (#926) * Add 'save image' button to the lightbox * Fix types * Fix types --- src/lib/media/manip.ts | 31 +++++++++++++++++++++++++++++- src/lib/media/manip.web.ts | 7 ++++++- src/view/com/lightbox/Lightbox.tsx | 24 +++++++++++++++++++++-- src/view/index.ts | 2 ++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/lib/media/manip.ts b/src/lib/media/manip.ts index c3595370..2f6c25e8 100644 --- a/src/lib/media/manip.ts +++ b/src/lib/media/manip.ts @@ -5,6 +5,7 @@ import {Image} from 'react-native-image-crop-picker' import * as RNFS from 'react-native-fs' import uuid from 'react-native-uuid' import * as Sharing from 'expo-sharing' +import * as MediaLibrary from 'expo-media-library' import {Dimensions} from './types' import {isAndroid, isIOS} from 'platform/detection' @@ -75,7 +76,7 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) { } } -export async function saveImageModal({uri}: {uri: string}) { +export async function shareImageModal({uri}: {uri: string}) { if (!(await Sharing.isAvailableAsync())) { // TODO might need to give an error to the user in this case -prf return @@ -107,6 +108,34 @@ export async function saveImageModal({uri}: {uri: string}) { RNFS.unlink(imagePath) } +export async function saveImageToAlbum({ + uri, + album, +}: { + uri: string + album: string +}) { + // download the file to cache + // NOTE + // assuming PNG + // we're currently relying on the fact our CDN only serves pngs + // -prf + const downloadResponse = await RNFetchBlob.config({ + fileCache: true, + }).fetch('GET', uri) + let imagePath = downloadResponse.path() + imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true) + + // save to the album (creating as needed) + const assetRef = await MediaLibrary.createAssetAsync(imagePath) + const albumRef = await MediaLibrary.getAlbumAsync(album) + if (albumRef) { + await MediaLibrary.addAssetsToAlbumAsync(assetRef, albumRef) + } else { + await MediaLibrary.createAlbumAsync(album, assetRef) + } +} + export function getImageDim(path: string): Promise<Dimensions> { return new Promise((resolve, reject) => { RNImage.getSize( diff --git a/src/lib/media/manip.web.ts b/src/lib/media/manip.web.ts index 464802c3..914b05d2 100644 --- a/src/lib/media/manip.web.ts +++ b/src/lib/media/manip.web.ts @@ -37,7 +37,12 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) { return await doResize(dataUri, opts) } -export async function saveImageModal(_opts: {uri: string}) { +export async function shareImageModal(_opts: {uri: string}) { + // TODO + throw new Error('TODO') +} + +export async function saveImageToAlbum(_opts: {uri: string; album: string}) { // TODO throw new Error('TODO') } diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index 18440c55..d1fd701c 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -5,7 +5,8 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import ImageView from './ImageViewing' import {useStores} from 'state/index' import * as models from 'state/models/ui/shell' -import {saveImageModal} from 'lib/media/manip' +import {shareImageModal, saveImageToAlbum} from 'lib/media/manip' +import * as Toast from '../util/Toast' import {Text} from '../util/text/Text' import {s, colors} from 'lib/styles' import {Button} from '../util/forms/Button' @@ -54,7 +55,16 @@ export const Lightbox = observer(function Lightbox() { <Button type="primary-outline" style={styles.footerBtn} - onPress={() => saveImageModal({uri})}> + onPress={() => saveImageToAlbumWithToasts(uri)}> + <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} /> + <Text type="xl" style={s.white}> + Save + </Text> + </Button> + <Button + type="primary-outline" + style={styles.footerBtn} + onPress={() => shareImageModal({uri})}> <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} /> <Text type="xl" style={s.white}> Share @@ -96,6 +106,15 @@ export const Lightbox = observer(function Lightbox() { } }) +async function saveImageToAlbumWithToasts(uri: string) { + try { + await saveImageToAlbum({uri, album: 'Bluesky'}) + Toast.show('Saved to the "Bluesky" album.') + } catch (e: any) { + Toast.show(`Failed to save image: ${String(e)}`) + } +} + const styles = StyleSheet.create({ footer: { paddingTop: 16, @@ -109,6 +128,7 @@ const styles = StyleSheet.create({ footerBtns: { flexDirection: 'row', justifyContent: 'center', + gap: 8, }, footerBtn: { flexDirection: 'row', diff --git a/src/view/index.ts b/src/view/index.ts index 118a6de2..0ab84fc0 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -37,6 +37,7 @@ import {faEnvelope} from '@fortawesome/free-solid-svg-icons/faEnvelope' import {faExclamation} from '@fortawesome/free-solid-svg-icons/faExclamation' import {faEye} from '@fortawesome/free-solid-svg-icons/faEye' import {faEyeSlash as farEyeSlash} from '@fortawesome/free-regular-svg-icons/faEyeSlash' +import {faFloppyDisk} from '@fortawesome/free-regular-svg-icons/faFloppyDisk' import {faGear} from '@fortawesome/free-solid-svg-icons/faGear' import {faGlobe} from '@fortawesome/free-solid-svg-icons/faGlobe' import {faHand} from '@fortawesome/free-solid-svg-icons/faHand' @@ -124,6 +125,7 @@ export function setup() { faEye, faExclamation, farEyeSlash, + faFloppyDisk, faGear, faGlobe, faHand,