1import { PermissionResponse, PermissionStatus, Platform } from 'expo-modules-core'; 2 3import { 4 ImagePickerAsset, 5 ImagePickerResult, 6 MediaTypeOptions, 7 OpenFileBrowserOptions, 8} from './ImagePicker.types'; 9 10const MediaTypeInput = { 11 [MediaTypeOptions.All]: 'video/mp4,video/quicktime,video/x-m4v,video/*,image/*', 12 [MediaTypeOptions.Images]: 'image/*', 13 [MediaTypeOptions.Videos]: 'video/mp4,video/quicktime,video/x-m4v,video/*', 14}; 15 16export default { 17 get name(): string { 18 return 'ExponentImagePicker'; 19 }, 20 21 async launchImageLibraryAsync({ 22 mediaTypes = MediaTypeOptions.Images, 23 allowsMultipleSelection = false, 24 base64 = false, 25 }): Promise<ImagePickerResult> { 26 // SSR guard 27 if (!Platform.isDOMAvailable) { 28 return { canceled: true, assets: null }; 29 } 30 return await openFileBrowserAsync({ 31 mediaTypes, 32 allowsMultipleSelection, 33 base64, 34 }); 35 }, 36 37 async launchCameraAsync({ 38 mediaTypes = MediaTypeOptions.Images, 39 allowsMultipleSelection = false, 40 base64 = false, 41 }): Promise<ImagePickerResult> { 42 // SSR guard 43 if (!Platform.isDOMAvailable) { 44 return { canceled: true, assets: null }; 45 } 46 return await openFileBrowserAsync({ 47 mediaTypes, 48 allowsMultipleSelection, 49 capture: true, 50 base64, 51 }); 52 }, 53 54 /* 55 * Delegate to expo-permissions to request camera permissions 56 */ 57 async getCameraPermissionsAsync() { 58 return permissionGrantedResponse(); 59 }, 60 async requestCameraPermissionsAsync() { 61 return permissionGrantedResponse(); 62 }, 63 64 /* 65 * Camera roll permissions don't need to be requested on web, so we always 66 * respond with granted. 67 */ 68 async getMediaLibraryPermissionsAsync(_writeOnly: boolean) { 69 return permissionGrantedResponse(); 70 }, 71 async requestMediaLibraryPermissionsAsync(_writeOnly: boolean): Promise<PermissionResponse> { 72 return permissionGrantedResponse(); 73 }, 74}; 75 76function permissionGrantedResponse(): PermissionResponse { 77 return { 78 status: PermissionStatus.GRANTED, 79 expires: 'never', 80 granted: true, 81 canAskAgain: true, 82 }; 83} 84 85function openFileBrowserAsync({ 86 mediaTypes, 87 capture = false, 88 allowsMultipleSelection = false, 89 base64, 90}: OpenFileBrowserOptions): Promise<ImagePickerResult> { 91 const mediaTypeFormat = MediaTypeInput[mediaTypes]; 92 93 const input = document.createElement('input'); 94 input.style.display = 'none'; 95 input.setAttribute('type', 'file'); 96 input.setAttribute('accept', mediaTypeFormat); 97 input.setAttribute('id', String(Math.random())); 98 if (allowsMultipleSelection) { 99 input.setAttribute('multiple', 'multiple'); 100 } 101 if (capture) { 102 input.setAttribute('capture', 'camera'); 103 } 104 document.body.appendChild(input); 105 106 return new Promise((resolve) => { 107 input.addEventListener('change', async () => { 108 if (input.files) { 109 const files = allowsMultipleSelection ? input.files : [input.files[0]]; 110 const assets: ImagePickerAsset[] = await Promise.all( 111 Array.from(files).map((file) => readFile(file, { base64 })) 112 ); 113 114 resolve({ canceled: false, assets }); 115 } else { 116 resolve({ canceled: true, assets: null }); 117 } 118 document.body.removeChild(input); 119 }); 120 121 const event = new MouseEvent('click'); 122 input.dispatchEvent(event); 123 }); 124} 125 126function readFile(targetFile: Blob, options: { base64: boolean }): Promise<ImagePickerAsset> { 127 return new Promise((resolve, reject) => { 128 const reader = new FileReader(); 129 reader.onerror = () => { 130 reject(new Error(`Failed to read the selected media because the operation failed.`)); 131 }; 132 reader.onload = ({ target }) => { 133 const uri = (target as any).result; 134 const returnRaw = () => resolve({ uri, width: 0, height: 0 }); 135 136 if (typeof uri === 'string') { 137 const image = new Image(); 138 image.src = uri; 139 image.onload = () => { 140 resolve({ 141 uri, 142 width: image.naturalWidth ?? image.width, 143 height: image.naturalHeight ?? image.height, 144 // The blob's result cannot be directly decoded as Base64 without 145 // first removing the Data-URL declaration preceding the 146 // Base64-encoded data. To retrieve only the Base64 encoded string, 147 // first remove data:*/*;base64, from the result. 148 // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL 149 ...(options.base64 && { base64: uri.substr(uri.indexOf(',') + 1) }), 150 }); 151 }; 152 image.onerror = () => returnRaw(); 153 } else { 154 returnRaw(); 155 } 156 }; 157 158 reader.readAsDataURL(targetFile); 159 }); 160} 161