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