expo-harmony-toolkit 1.7.1 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +20 -9
- package/README.md +22 -11
- package/build/core/autolinking.d.ts +10 -0
- package/build/core/autolinking.js +421 -0
- package/build/core/build/commands.d.ts +19 -0
- package/build/core/build/commands.js +61 -0
- package/build/core/build/compatibilityShims.d.ts +1 -0
- package/build/core/build/compatibilityShims.js +272 -0
- package/build/core/build/localHar.d.ts +13 -0
- package/build/core/build/localHar.js +337 -0
- package/build/core/build/reporting.d.ts +19 -0
- package/build/core/build/reporting.js +67 -0
- package/build/core/build/rnohCompatibility.d.ts +1 -0
- package/build/core/build/rnohCompatibility.js +38 -0
- package/build/core/build.d.ts +4 -12
- package/build/core/build.js +70 -762
- package/build/core/constants.d.ts +2 -1
- package/build/core/constants.js +3 -2
- package/build/core/dependencyInspection.d.ts +2 -0
- package/build/core/dependencyInspection.js +112 -0
- package/build/core/env.js +1 -1
- package/build/core/report.js +33 -10
- package/build/core/signing.d.ts +2 -0
- package/build/core/signing.js +30 -0
- package/build/core/template/capabilityRegistry.d.ts +9 -0
- package/build/core/template/capabilityRegistry.js +84 -0
- package/build/core/template/expoModulesCoreShim.d.ts +2 -0
- package/build/core/template/expoModulesCoreShim.js +279 -0
- package/build/core/template/metro.d.ts +2 -0
- package/build/core/template/metro.js +213 -0
- package/build/core/template/nativeFiles.d.ts +8 -0
- package/build/core/template/nativeFiles.js +333 -0
- package/build/core/template/renderers/camera.d.ts +4 -0
- package/build/core/template/renderers/camera.js +612 -0
- package/build/core/template/renderers/fileSystem.d.ts +4 -0
- package/build/core/template/renderers/fileSystem.js +814 -0
- package/build/core/template/renderers/imagePicker.d.ts +4 -0
- package/build/core/template/renderers/imagePicker.js +753 -0
- package/build/core/template/renderers/location.d.ts +4 -0
- package/build/core/template/renderers/location.js +836 -0
- package/build/core/template/runtimeShims.d.ts +2 -0
- package/build/core/template/runtimeShims.js +192 -0
- package/build/core/template/support.d.ts +11 -0
- package/build/core/template/support.js +108 -0
- package/build/core/template.d.ts +2 -6
- package/build/core/template.js +47 -3966
- package/build/data/capabilities.js +45 -19
- package/build/data/dependencyCatalog.js +10 -0
- package/build/data/publicDocs.d.ts +11 -0
- package/build/data/publicDocs.js +28 -0
- package/build/data/validatedMatrices.js +9 -1
- package/build/docs/render.d.ts +9 -0
- package/build/docs/render.js +248 -0
- package/build/types.d.ts +12 -0
- package/docs/cli-build.md +1 -1
- package/docs/npm-release.md +1 -0
- package/docs/official-app-shell-sample.md +2 -1
- package/docs/official-minimal-sample.md +2 -1
- package/docs/official-native-capabilities-sample.md +28 -47
- package/docs/official-ui-stack-sample.md +2 -1
- package/docs/signing-and-release.md +3 -0
- package/docs/support-matrix.md +38 -16
- package/package.json +13 -5
- package/docs/v1.5.1-acceptance.md +0 -385
- package/docs/v1.6.0-acceptance.md +0 -193
- package/docs/v1.7.0-acceptance.md +0 -111
- package/docs/v1.7.1-acceptance.md +0 -111
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderExpoHarmonyImagePickerTurboModule = renderExpoHarmonyImagePickerTurboModule;
|
|
4
|
+
exports.renderExpoImagePickerHarmonyAdapterShim = renderExpoImagePickerHarmonyAdapterShim;
|
|
5
|
+
exports.renderExpoImagePickerPreviewShim = renderExpoImagePickerPreviewShim;
|
|
6
|
+
function renderExpoHarmonyImagePickerTurboModule() {
|
|
7
|
+
return `import type { Permissions } from '@ohos.abilityAccessCtrl';
|
|
8
|
+
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
|
|
9
|
+
import photoAccessHelper from '@ohos.file.photoAccessHelper';
|
|
10
|
+
import picker from '@ohos.file.picker';
|
|
11
|
+
import image from '@ohos.multimedia.image';
|
|
12
|
+
import fs from '@ohos.file.fs';
|
|
13
|
+
import { UITurboModuleContext, UITurboModule } from '@rnoh/react-native-openharmony/ts';
|
|
14
|
+
|
|
15
|
+
type PermissionResponse = {
|
|
16
|
+
status: 'granted' | 'denied' | 'undetermined';
|
|
17
|
+
granted: boolean;
|
|
18
|
+
canAskAgain: boolean;
|
|
19
|
+
expires: 'never';
|
|
20
|
+
accessPrivileges?: 'all' | 'limited' | 'none';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type LaunchImageLibraryOptions = {
|
|
24
|
+
mediaTypes?: string | string[];
|
|
25
|
+
allowsMultipleSelection?: boolean;
|
|
26
|
+
selectionLimit?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type LaunchCameraOptions = {
|
|
30
|
+
mediaTypes?: string | string[];
|
|
31
|
+
cameraType?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ImagePickerAsset = {
|
|
35
|
+
uri: string;
|
|
36
|
+
assetId: string | null;
|
|
37
|
+
width: number;
|
|
38
|
+
height: number;
|
|
39
|
+
type: 'image' | 'video' | null;
|
|
40
|
+
fileName: string | null;
|
|
41
|
+
fileSize: number | null;
|
|
42
|
+
mimeType: string | null;
|
|
43
|
+
duration: number | null;
|
|
44
|
+
exif: null;
|
|
45
|
+
base64: null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type ImagePickerResult = {
|
|
49
|
+
canceled: boolean;
|
|
50
|
+
assets: ImagePickerAsset[] | null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export class ExpoHarmonyImagePickerTurboModule extends UITurboModule {
|
|
54
|
+
public static readonly NAME = 'ExpoHarmonyImagePicker';
|
|
55
|
+
|
|
56
|
+
private readonly atManager = abilityAccessCtrl.createAtManager();
|
|
57
|
+
private pendingResult: ImagePickerResult | null = null;
|
|
58
|
+
|
|
59
|
+
getConstants(): Record<string, never> {
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getMediaLibraryPermissionStatus(_writeOnly?: boolean): Promise<PermissionResponse> {
|
|
64
|
+
return this.getPermissionResponse('ohos.permission.READ_IMAGEVIDEO', true);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async requestMediaLibraryPermission(_writeOnly?: boolean): Promise<PermissionResponse> {
|
|
68
|
+
return this.requestPermissionResponse('ohos.permission.READ_IMAGEVIDEO', true);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getCameraPermissionStatus(): Promise<PermissionResponse> {
|
|
72
|
+
return this.getPermissionResponse('ohos.permission.CAMERA', false);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async requestCameraPermission(): Promise<PermissionResponse> {
|
|
76
|
+
return this.requestPermissionResponse('ohos.permission.CAMERA', false);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async launchImageLibrary(options?: LaunchImageLibraryOptions): Promise<ImagePickerResult> {
|
|
80
|
+
await this.ensurePermissionGranted('ohos.permission.READ_IMAGEVIDEO', true);
|
|
81
|
+
|
|
82
|
+
const photoPicker = new photoAccessHelper.PhotoViewPicker();
|
|
83
|
+
const selection = await photoPicker.select(this.createPhotoSelectOptions(options));
|
|
84
|
+
let selectedUris = this.normalizeSelectedUris(selection?.photoUris);
|
|
85
|
+
|
|
86
|
+
if (selectedUris.length === 0) {
|
|
87
|
+
selectedUris = await this.launchLegacyPhotoPicker(options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (selectedUris.length === 0) {
|
|
91
|
+
return this.createCanceledResult();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const authorizedUris = await this.requestAuthorizedUris(selectedUris);
|
|
95
|
+
const assets = await Promise.all(
|
|
96
|
+
authorizedUris.map((uri, index) =>
|
|
97
|
+
this.createImagePickerAsset(uri, selectedUris[index] ?? uri, this.inferAssetTypeFromMediaTypes(options?.mediaTypes)),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const result = {
|
|
102
|
+
canceled: false,
|
|
103
|
+
assets,
|
|
104
|
+
};
|
|
105
|
+
this.pendingResult = result;
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async launchCamera(options?: LaunchCameraOptions): Promise<ImagePickerResult> {
|
|
110
|
+
const requestedAssetType = this.inferAssetTypeFromMediaTypes(options?.mediaTypes);
|
|
111
|
+
|
|
112
|
+
await this.ensurePermissionGranted('ohos.permission.CAMERA', false);
|
|
113
|
+
await this.ensurePermissionGranted('ohos.permission.READ_IMAGEVIDEO', true);
|
|
114
|
+
if (requestedAssetType === 'video') {
|
|
115
|
+
await this.ensurePermissionGranted('ohos.permission.MICROPHONE', false);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const cameraEntryPicker = new photoAccessHelper.PhotoViewPicker();
|
|
119
|
+
const selection = await cameraEntryPicker.select(
|
|
120
|
+
this.createPhotoSelectOptions(
|
|
121
|
+
{
|
|
122
|
+
mediaTypes: requestedAssetType === 'video' ? 'videos' : 'images',
|
|
123
|
+
allowsMultipleSelection: false,
|
|
124
|
+
},
|
|
125
|
+
true,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
const selectedUris = this.normalizeSelectedUris(selection?.photoUris);
|
|
129
|
+
|
|
130
|
+
if (selectedUris.length === 0) {
|
|
131
|
+
return this.createCanceledResult();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const authorizedUris = await this.requestAuthorizedUris(selectedUris);
|
|
135
|
+
const assetUri = authorizedUris[0] ?? selectedUris[0];
|
|
136
|
+
|
|
137
|
+
const result = {
|
|
138
|
+
canceled: false,
|
|
139
|
+
assets: [await this.createImagePickerAsset(assetUri, selectedUris[0], requestedAssetType ?? 'image')],
|
|
140
|
+
};
|
|
141
|
+
this.pendingResult = result;
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async getPendingResult(): Promise<ImagePickerResult | null> {
|
|
146
|
+
const result = this.pendingResult;
|
|
147
|
+
this.pendingResult = null;
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async ensurePermissionGranted(
|
|
152
|
+
permissionName: Permissions,
|
|
153
|
+
isMediaLibraryPermission: boolean,
|
|
154
|
+
): Promise<void> {
|
|
155
|
+
const permissionResponse = await this.getPermissionResponse(permissionName, isMediaLibraryPermission);
|
|
156
|
+
|
|
157
|
+
if (permissionResponse.granted) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const requestedResponse = await this.requestPermissionResponse(permissionName, isMediaLibraryPermission);
|
|
162
|
+
|
|
163
|
+
if (!requestedResponse.granted) {
|
|
164
|
+
throw new Error(\`Permission denied for \${permissionName}.\`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async getPermissionResponse(
|
|
169
|
+
permissionName: Permissions,
|
|
170
|
+
isMediaLibraryPermission: boolean,
|
|
171
|
+
): Promise<PermissionResponse> {
|
|
172
|
+
return this.permissionResponseFromStatus(
|
|
173
|
+
this.resolvePermissionStatus(permissionName),
|
|
174
|
+
isMediaLibraryPermission,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async requestPermissionResponse(
|
|
179
|
+
permissionName: Permissions,
|
|
180
|
+
isMediaLibraryPermission: boolean,
|
|
181
|
+
): Promise<PermissionResponse> {
|
|
182
|
+
const requestResult = await this.atManager.requestPermissionsFromUser(
|
|
183
|
+
this.ctx.uiAbilityContext,
|
|
184
|
+
[permissionName],
|
|
185
|
+
);
|
|
186
|
+
const authResult = Array.isArray(requestResult.authResults)
|
|
187
|
+
? Number(requestResult.authResults[0] ?? abilityAccessCtrl.GrantStatus.PERMISSION_DENIED)
|
|
188
|
+
: abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
|
|
189
|
+
|
|
190
|
+
if (authResult === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
|
|
191
|
+
return this.permissionResponseFromStatus(
|
|
192
|
+
abilityAccessCtrl.PermissionStatus.GRANTED,
|
|
193
|
+
isMediaLibraryPermission,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return this.permissionResponseFromStatus(
|
|
198
|
+
abilityAccessCtrl.PermissionStatus.DENIED,
|
|
199
|
+
isMediaLibraryPermission,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private resolvePermissionStatus(permissionName: Permissions): abilityAccessCtrl.PermissionStatus {
|
|
204
|
+
const atManagerWithSelfStatus = this.atManager as abilityAccessCtrl.AtManager & {
|
|
205
|
+
getSelfPermissionStatus?: (permission: Permissions) => abilityAccessCtrl.PermissionStatus;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (typeof atManagerWithSelfStatus.getSelfPermissionStatus === 'function') {
|
|
209
|
+
return atManagerWithSelfStatus.getSelfPermissionStatus(permissionName);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const accessTokenId = this.ctx.uiAbilityContext.abilityInfo.applicationInfo.accessTokenId;
|
|
213
|
+
const grantStatus = this.atManager.checkAccessTokenSync(accessTokenId, permissionName);
|
|
214
|
+
|
|
215
|
+
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
|
|
216
|
+
? abilityAccessCtrl.PermissionStatus.GRANTED
|
|
217
|
+
: abilityAccessCtrl.PermissionStatus.NOT_DETERMINED;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private permissionResponseFromStatus(
|
|
221
|
+
permissionStatus: abilityAccessCtrl.PermissionStatus,
|
|
222
|
+
isMediaLibraryPermission: boolean,
|
|
223
|
+
): PermissionResponse {
|
|
224
|
+
const granted = permissionStatus === abilityAccessCtrl.PermissionStatus.GRANTED;
|
|
225
|
+
const denied =
|
|
226
|
+
permissionStatus === abilityAccessCtrl.PermissionStatus.DENIED ||
|
|
227
|
+
permissionStatus === abilityAccessCtrl.PermissionStatus.RESTRICTED ||
|
|
228
|
+
permissionStatus === abilityAccessCtrl.PermissionStatus.INVALID;
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
status: granted ? 'granted' : denied ? 'denied' : 'undetermined',
|
|
232
|
+
granted,
|
|
233
|
+
canAskAgain: !denied,
|
|
234
|
+
expires: 'never',
|
|
235
|
+
...(isMediaLibraryPermission
|
|
236
|
+
? {
|
|
237
|
+
accessPrivileges: granted ? 'all' : 'none',
|
|
238
|
+
}
|
|
239
|
+
: {}),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private createPhotoSelectOptions(
|
|
244
|
+
options?: LaunchImageLibraryOptions,
|
|
245
|
+
isPhotoTakingSupported: boolean = false,
|
|
246
|
+
): photoAccessHelper.PhotoSelectOptions {
|
|
247
|
+
const selectOptions = new photoAccessHelper.PhotoSelectOptions();
|
|
248
|
+
selectOptions.MIMEType = this.resolvePhotoViewMimeType(options?.mediaTypes);
|
|
249
|
+
selectOptions.maxSelectNumber =
|
|
250
|
+
options?.allowsMultipleSelection === true
|
|
251
|
+
? this.resolveSelectionLimit(options?.selectionLimit)
|
|
252
|
+
: 1;
|
|
253
|
+
selectOptions.isSearchSupported = true;
|
|
254
|
+
selectOptions.isPhotoTakingSupported = isPhotoTakingSupported;
|
|
255
|
+
return selectOptions;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async launchLegacyPhotoPicker(options?: LaunchImageLibraryOptions): Promise<string[]> {
|
|
259
|
+
const legacyPicker = new picker.PhotoViewPicker(this.ctx.uiAbilityContext);
|
|
260
|
+
const legacyOptions = new picker.PhotoSelectOptions();
|
|
261
|
+
legacyOptions.MIMEType = this.resolveLegacyPhotoViewMimeType(options?.mediaTypes);
|
|
262
|
+
legacyOptions.maxSelectNumber =
|
|
263
|
+
options?.allowsMultipleSelection === true
|
|
264
|
+
? this.resolveSelectionLimit(options?.selectionLimit)
|
|
265
|
+
: 1;
|
|
266
|
+
|
|
267
|
+
const selection = await legacyPicker.select(legacyOptions);
|
|
268
|
+
return this.normalizeSelectedUris(selection?.photoUris);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private resolveSelectionLimit(selectionLimit?: number): number {
|
|
272
|
+
if (typeof selectionLimit === 'number' && Number.isFinite(selectionLimit) && selectionLimit > 0) {
|
|
273
|
+
return Math.floor(selectionLimit);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return 20;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private resolvePhotoViewMimeType(
|
|
280
|
+
rawMediaTypes?: string | string[],
|
|
281
|
+
): photoAccessHelper.PhotoViewMIMETypes {
|
|
282
|
+
const normalized = this.normalizeMediaTypes(rawMediaTypes);
|
|
283
|
+
|
|
284
|
+
if (normalized.includes('video') && !normalized.includes('image')) {
|
|
285
|
+
return photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (normalized.includes('video') && normalized.includes('image')) {
|
|
289
|
+
return photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private resolveLegacyPhotoViewMimeType(
|
|
296
|
+
rawMediaTypes?: string | string[],
|
|
297
|
+
): picker.PhotoViewMIMETypes {
|
|
298
|
+
const normalized = this.normalizeMediaTypes(rawMediaTypes);
|
|
299
|
+
|
|
300
|
+
if (normalized.includes('video') && !normalized.includes('image')) {
|
|
301
|
+
return picker.PhotoViewMIMETypes.VIDEO_TYPE;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (normalized.includes('video') && normalized.includes('image')) {
|
|
305
|
+
return picker.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return picker.PhotoViewMIMETypes.IMAGE_TYPE;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private inferAssetTypeFromMediaTypes(
|
|
312
|
+
rawMediaTypes?: string | string[],
|
|
313
|
+
): 'image' | 'video' | null {
|
|
314
|
+
const normalized = this.normalizeMediaTypes(rawMediaTypes);
|
|
315
|
+
|
|
316
|
+
if (normalized.includes('video') && !normalized.includes('image')) {
|
|
317
|
+
return 'video';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (normalized.includes('image')) {
|
|
321
|
+
return 'image';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private normalizeMediaTypes(rawMediaTypes?: string | string[]): string[] {
|
|
328
|
+
if (Array.isArray(rawMediaTypes)) {
|
|
329
|
+
return Array.from(
|
|
330
|
+
new Set(
|
|
331
|
+
rawMediaTypes
|
|
332
|
+
.map((value) => this.normalizeMediaTypeValue(value))
|
|
333
|
+
.filter((value): value is string => value !== null),
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const singleValue = this.normalizeMediaTypeValue(rawMediaTypes);
|
|
339
|
+
return singleValue ? [singleValue] : ['image'];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private normalizeMediaTypeValue(rawValue?: string): string | null {
|
|
343
|
+
if (typeof rawValue !== 'string' || rawValue.length === 0) {
|
|
344
|
+
return 'image';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
switch (rawValue) {
|
|
348
|
+
case 'All':
|
|
349
|
+
case 'all':
|
|
350
|
+
case 'images':
|
|
351
|
+
case 'image':
|
|
352
|
+
case 'livePhotos':
|
|
353
|
+
return 'image';
|
|
354
|
+
case 'Videos':
|
|
355
|
+
case 'videos':
|
|
356
|
+
case 'video':
|
|
357
|
+
return 'video';
|
|
358
|
+
default:
|
|
359
|
+
return rawValue.includes('video') ? 'video' : rawValue.includes('image') ? 'image' : null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private normalizeSelectedUris(photoUris: Array<string> | undefined | null): string[] {
|
|
364
|
+
if (!Array.isArray(photoUris)) {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return photoUris.filter(
|
|
369
|
+
(value): value is string => typeof value === 'string' && value.length > 0,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private async requestAuthorizedUris(photoUris: string[]): Promise<string[]> {
|
|
374
|
+
const helper = photoAccessHelper.getPhotoAccessHelper(this.ctx.uiAbilityContext);
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const authorizedUris = await helper.requestPhotoUrisReadPermission(photoUris);
|
|
378
|
+
const normalizedAuthorizedUris = this.normalizeSelectedUris(authorizedUris);
|
|
379
|
+
return normalizedAuthorizedUris.length > 0 ? normalizedAuthorizedUris : photoUris;
|
|
380
|
+
} catch (_error) {
|
|
381
|
+
return photoUris;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private async createImagePickerAsset(
|
|
386
|
+
assetUri: string,
|
|
387
|
+
originalUri: string,
|
|
388
|
+
fallbackType: 'image' | 'video' | null,
|
|
389
|
+
): Promise<ImagePickerAsset> {
|
|
390
|
+
const inferredType = this.inferAssetTypeFromUri(assetUri, fallbackType);
|
|
391
|
+
const imageSize =
|
|
392
|
+
inferredType === 'image' ? await this.getImageSize(assetUri) : { width: 0, height: 0 };
|
|
393
|
+
const fileSize = await this.getFileSize(assetUri);
|
|
394
|
+
const fileName = this.extractFileName(assetUri) ?? this.extractFileName(originalUri);
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
uri: assetUri,
|
|
398
|
+
assetId: originalUri,
|
|
399
|
+
width: imageSize.width,
|
|
400
|
+
height: imageSize.height,
|
|
401
|
+
type: inferredType,
|
|
402
|
+
fileName,
|
|
403
|
+
fileSize,
|
|
404
|
+
mimeType: this.inferMimeType(assetUri, inferredType),
|
|
405
|
+
duration: inferredType === 'video' ? await this.getVideoDuration(assetUri) : null,
|
|
406
|
+
exif: null,
|
|
407
|
+
base64: null,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private inferAssetTypeFromUri(
|
|
412
|
+
assetUri: string,
|
|
413
|
+
fallbackType: 'image' | 'video' | null,
|
|
414
|
+
): 'image' | 'video' | null {
|
|
415
|
+
const normalizedUri = assetUri.toLowerCase();
|
|
416
|
+
|
|
417
|
+
if (normalizedUri.match(/\\.(mp4|m4v|mov|3gp|webm)(\\?|#|$)/)) {
|
|
418
|
+
return 'video';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (normalizedUri.match(/\\.(png|jpe?g|gif|bmp|webp|heic|heif)(\\?|#|$)/)) {
|
|
422
|
+
return 'image';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return fallbackType;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private inferMimeType(
|
|
429
|
+
assetUri: string,
|
|
430
|
+
assetType: 'image' | 'video' | null,
|
|
431
|
+
): string | null {
|
|
432
|
+
const normalizedUri = assetUri.toLowerCase();
|
|
433
|
+
|
|
434
|
+
if (normalizedUri.endsWith('.png')) {
|
|
435
|
+
return 'image/png';
|
|
436
|
+
}
|
|
437
|
+
if (normalizedUri.endsWith('.gif')) {
|
|
438
|
+
return 'image/gif';
|
|
439
|
+
}
|
|
440
|
+
if (normalizedUri.endsWith('.webp')) {
|
|
441
|
+
return 'image/webp';
|
|
442
|
+
}
|
|
443
|
+
if (normalizedUri.endsWith('.bmp')) {
|
|
444
|
+
return 'image/bmp';
|
|
445
|
+
}
|
|
446
|
+
if (normalizedUri.endsWith('.heic')) {
|
|
447
|
+
return 'image/heic';
|
|
448
|
+
}
|
|
449
|
+
if (normalizedUri.endsWith('.heif')) {
|
|
450
|
+
return 'image/heif';
|
|
451
|
+
}
|
|
452
|
+
if (normalizedUri.match(/\\.jpe?g(\\?|#|$)/)) {
|
|
453
|
+
return 'image/jpeg';
|
|
454
|
+
}
|
|
455
|
+
if (normalizedUri.match(/\\.(mp4|m4v)(\\?|#|$)/)) {
|
|
456
|
+
return 'video/mp4';
|
|
457
|
+
}
|
|
458
|
+
if (normalizedUri.match(/\\.mov(\\?|#|$)/)) {
|
|
459
|
+
return 'video/quicktime';
|
|
460
|
+
}
|
|
461
|
+
if (normalizedUri.match(/\\.webm(\\?|#|$)/)) {
|
|
462
|
+
return 'video/webm';
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return assetType === 'video' ? 'video/*' : assetType === 'image' ? 'image/*' : null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private extractFileName(assetUri: string): string | null {
|
|
469
|
+
if (typeof assetUri !== 'string' || assetUri.length === 0) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const sanitizedUri = assetUri.split('?')[0]?.split('#')[0] ?? assetUri;
|
|
474
|
+
const lastSlashIndex = sanitizedUri.lastIndexOf('/');
|
|
475
|
+
const rawFileName =
|
|
476
|
+
lastSlashIndex >= 0 ? sanitizedUri.slice(lastSlashIndex + 1) : sanitizedUri;
|
|
477
|
+
|
|
478
|
+
if (rawFileName.length === 0) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
return decodeURIComponent(rawFileName);
|
|
484
|
+
} catch (_error) {
|
|
485
|
+
return rawFileName;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private async getImageSize(assetUri: string): Promise<{ width: number; height: number }> {
|
|
490
|
+
let imageSource: image.ImageSource | null = null;
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
imageSource = image.createImageSource(assetUri);
|
|
494
|
+
const imageInfo = await imageSource.getImageInfo();
|
|
495
|
+
return {
|
|
496
|
+
width: Number(imageInfo.size?.width ?? 0),
|
|
497
|
+
height: Number(imageInfo.size?.height ?? 0),
|
|
498
|
+
};
|
|
499
|
+
} catch (_error) {
|
|
500
|
+
return {
|
|
501
|
+
width: 0,
|
|
502
|
+
height: 0,
|
|
503
|
+
};
|
|
504
|
+
} finally {
|
|
505
|
+
if (imageSource) {
|
|
506
|
+
try {
|
|
507
|
+
await imageSource.release();
|
|
508
|
+
} catch (_error) {
|
|
509
|
+
// Ignore cleanup errors from ImageSource release.
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private async getFileSize(assetUri: string): Promise<number | null> {
|
|
516
|
+
const fsTarget = this.resolveFsTarget(assetUri);
|
|
517
|
+
|
|
518
|
+
if (!fsTarget) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
const stat = await fs.stat(fsTarget);
|
|
524
|
+
return Number(stat.size ?? 0);
|
|
525
|
+
} catch (_error) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private async getVideoDuration(_assetUri: string): Promise<number | null> {
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private resolveFsTarget(assetUri: string): string | null {
|
|
535
|
+
if (assetUri.startsWith('file://')) {
|
|
536
|
+
return assetUri.slice('file://'.length);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return assetUri.startsWith('/') ? assetUri : null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private createCanceledResult(): ImagePickerResult {
|
|
543
|
+
return {
|
|
544
|
+
canceled: true,
|
|
545
|
+
assets: null,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
`;
|
|
550
|
+
}
|
|
551
|
+
function renderExpoImagePickerHarmonyAdapterShim(capability) {
|
|
552
|
+
return `'use strict';
|
|
553
|
+
|
|
554
|
+
const { TurboModuleRegistry } = require('react-native');
|
|
555
|
+
const { CodedError } = require('expo-modules-core');
|
|
556
|
+
|
|
557
|
+
const NATIVE_MODULE_NAME = 'ExpoHarmonyImagePicker';
|
|
558
|
+
const NATIVE_MODULE = TurboModuleRegistry.get(NATIVE_MODULE_NAME);
|
|
559
|
+
|
|
560
|
+
function createError(code, message) {
|
|
561
|
+
return new CodedError(code, message);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function requireNativeModule(operationName) {
|
|
565
|
+
if (NATIVE_MODULE) {
|
|
566
|
+
return NATIVE_MODULE;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
throw createError(
|
|
570
|
+
'ERR_EXPO_HARMONY_NATIVE_MODULE_MISSING',
|
|
571
|
+
'${capability.packageName} expected the ' +
|
|
572
|
+
NATIVE_MODULE_NAME +
|
|
573
|
+
' TurboModule to be registered, but it was missing while running ' +
|
|
574
|
+
operationName +
|
|
575
|
+
'.',
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function normalizeNativeError(error) {
|
|
580
|
+
if (error instanceof Error) {
|
|
581
|
+
return error;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (error && typeof error === 'object') {
|
|
585
|
+
const code =
|
|
586
|
+
typeof error.code === 'number' || typeof error.code === 'string'
|
|
587
|
+
? String(error.code)
|
|
588
|
+
: null;
|
|
589
|
+
const message =
|
|
590
|
+
typeof error.message === 'string' && error.message.length > 0
|
|
591
|
+
? error.message
|
|
592
|
+
: typeof error.name === 'string' && error.name.length > 0
|
|
593
|
+
? error.name
|
|
594
|
+
: JSON.stringify(error);
|
|
595
|
+
|
|
596
|
+
return new Error(code ? '[native:' + code + '] ' + message : message);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return new Error(String(error));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function normalizePickerResult(result) {
|
|
603
|
+
if (!result || result.canceled === true || !Array.isArray(result.assets) || result.assets.length === 0) {
|
|
604
|
+
return {
|
|
605
|
+
canceled: true,
|
|
606
|
+
assets: null,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
canceled: false,
|
|
612
|
+
assets: result.assets.map((asset) => ({
|
|
613
|
+
uri: String(asset.uri),
|
|
614
|
+
assetId: asset.assetId ?? null,
|
|
615
|
+
width: Number(asset.width ?? 0),
|
|
616
|
+
height: Number(asset.height ?? 0),
|
|
617
|
+
type: asset.type ?? null,
|
|
618
|
+
fileName: asset.fileName ?? null,
|
|
619
|
+
fileSize:
|
|
620
|
+
typeof asset.fileSize === 'number' && Number.isFinite(asset.fileSize)
|
|
621
|
+
? asset.fileSize
|
|
622
|
+
: null,
|
|
623
|
+
mimeType: asset.mimeType ?? null,
|
|
624
|
+
duration:
|
|
625
|
+
typeof asset.duration === 'number' && Number.isFinite(asset.duration)
|
|
626
|
+
? asset.duration
|
|
627
|
+
: null,
|
|
628
|
+
exif: asset.exif ?? null,
|
|
629
|
+
base64: asset.base64 ?? null,
|
|
630
|
+
})),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function invokeNative(methodName, operationName, ...args) {
|
|
635
|
+
try {
|
|
636
|
+
return await requireNativeModule(operationName)[methodName](...args);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
throw normalizeNativeError(error);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
module.exports = {
|
|
643
|
+
MediaTypeOptions: {
|
|
644
|
+
All: 'All',
|
|
645
|
+
Images: 'Images',
|
|
646
|
+
Videos: 'Videos',
|
|
647
|
+
},
|
|
648
|
+
CameraType: {
|
|
649
|
+
front: 'front',
|
|
650
|
+
back: 'back',
|
|
651
|
+
},
|
|
652
|
+
UIImagePickerPresentationStyle: {
|
|
653
|
+
AUTOMATIC: 'automatic',
|
|
654
|
+
FULL_SCREEN: 'fullScreen',
|
|
655
|
+
PAGE_SHEET: 'pageSheet',
|
|
656
|
+
FORM_SHEET: 'formSheet',
|
|
657
|
+
CURRENT_CONTEXT: 'currentContext',
|
|
658
|
+
OVER_FULL_SCREEN: 'overFullScreen',
|
|
659
|
+
},
|
|
660
|
+
async requestCameraPermissionsAsync() {
|
|
661
|
+
return invokeNative(
|
|
662
|
+
'requestCameraPermission',
|
|
663
|
+
'requestCameraPermissionsAsync',
|
|
664
|
+
);
|
|
665
|
+
},
|
|
666
|
+
async requestMediaLibraryPermissionsAsync(writeOnly) {
|
|
667
|
+
return invokeNative(
|
|
668
|
+
'requestMediaLibraryPermission',
|
|
669
|
+
'requestMediaLibraryPermissionsAsync',
|
|
670
|
+
writeOnly === true,
|
|
671
|
+
);
|
|
672
|
+
},
|
|
673
|
+
async getCameraPermissionsAsync() {
|
|
674
|
+
return invokeNative('getCameraPermissionStatus', 'getCameraPermissionsAsync');
|
|
675
|
+
},
|
|
676
|
+
async getMediaLibraryPermissionsAsync(writeOnly) {
|
|
677
|
+
return invokeNative(
|
|
678
|
+
'getMediaLibraryPermissionStatus',
|
|
679
|
+
'getMediaLibraryPermissionsAsync',
|
|
680
|
+
writeOnly === true,
|
|
681
|
+
);
|
|
682
|
+
},
|
|
683
|
+
async launchCameraAsync(options) {
|
|
684
|
+
return normalizePickerResult(
|
|
685
|
+
await invokeNative('launchCamera', 'launchCameraAsync', options ?? {}),
|
|
686
|
+
);
|
|
687
|
+
},
|
|
688
|
+
async launchImageLibraryAsync(options) {
|
|
689
|
+
return normalizePickerResult(
|
|
690
|
+
await invokeNative('launchImageLibrary', 'launchImageLibraryAsync', options ?? {}),
|
|
691
|
+
);
|
|
692
|
+
},
|
|
693
|
+
async getPendingResultAsync() {
|
|
694
|
+
const result = await invokeNative('getPendingResult', 'getPendingResultAsync');
|
|
695
|
+
return result ? normalizePickerResult(result) : null;
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
`;
|
|
699
|
+
}
|
|
700
|
+
function renderExpoImagePickerPreviewShim(capability) {
|
|
701
|
+
return `'use strict';
|
|
702
|
+
|
|
703
|
+
const { CodedError } = require('expo-modules-core');
|
|
704
|
+
|
|
705
|
+
const PREVIEW_MESSAGE =
|
|
706
|
+
'${capability.packageName} is routed through the Expo Harmony ${capability.supportTier} bridge. Bundling is supported, but picker and camera flows still need device-side validation.';
|
|
707
|
+
|
|
708
|
+
function createPreviewError(operationName) {
|
|
709
|
+
return new CodedError(
|
|
710
|
+
'ERR_EXPO_HARMONY_PREVIEW',
|
|
711
|
+
PREVIEW_MESSAGE + ' Attempted operation: ' + operationName + '.',
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function unavailable(operationName) {
|
|
716
|
+
throw createPreviewError(operationName);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
module.exports = {
|
|
720
|
+
MediaTypeOptions: {
|
|
721
|
+
All: 'All',
|
|
722
|
+
Images: 'Images',
|
|
723
|
+
Videos: 'Videos',
|
|
724
|
+
},
|
|
725
|
+
UIImagePickerPresentationStyle: {
|
|
726
|
+
AUTOMATIC: 'automatic',
|
|
727
|
+
FULL_SCREEN: 'fullScreen',
|
|
728
|
+
PAGE_SHEET: 'pageSheet',
|
|
729
|
+
FORM_SHEET: 'formSheet',
|
|
730
|
+
CURRENT_CONTEXT: 'currentContext',
|
|
731
|
+
OVER_FULL_SCREEN: 'overFullScreen',
|
|
732
|
+
},
|
|
733
|
+
requestCameraPermissionsAsync() {
|
|
734
|
+
return unavailable('requestCameraPermissionsAsync');
|
|
735
|
+
},
|
|
736
|
+
requestMediaLibraryPermissionsAsync() {
|
|
737
|
+
return unavailable('requestMediaLibraryPermissionsAsync');
|
|
738
|
+
},
|
|
739
|
+
getCameraPermissionsAsync() {
|
|
740
|
+
return unavailable('getCameraPermissionsAsync');
|
|
741
|
+
},
|
|
742
|
+
getMediaLibraryPermissionsAsync() {
|
|
743
|
+
return unavailable('getMediaLibraryPermissionsAsync');
|
|
744
|
+
},
|
|
745
|
+
launchCameraAsync(options) {
|
|
746
|
+
return unavailable('launchCameraAsync(' + JSON.stringify(options ?? {}) + ')');
|
|
747
|
+
},
|
|
748
|
+
launchImageLibraryAsync(options) {
|
|
749
|
+
return unavailable('launchImageLibraryAsync(' + JSON.stringify(options ?? {}) + ')');
|
|
750
|
+
},
|
|
751
|
+
};
|
|
752
|
+
`;
|
|
753
|
+
}
|