@symbiote-native/media-library 0.1.0

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -0
  3. package/build/angular/index.d.ts +1 -0
  4. package/build/angular/index.js +1 -0
  5. package/build/core/index.d.ts +3 -0
  6. package/build/core/index.js +3 -0
  7. package/build/core/media-library.d.ts +94 -0
  8. package/build/core/media-library.js +284 -0
  9. package/build/core/native-module.d.ts +29 -0
  10. package/build/core/native-module.js +3 -0
  11. package/build/core/types.d.ts +126 -0
  12. package/build/core/types.js +1 -0
  13. package/build/next/album.d.ts +9 -0
  14. package/build/next/album.js +12 -0
  15. package/build/next/asset.d.ts +13 -0
  16. package/build/next/asset.js +34 -0
  17. package/build/next/index.d.ts +24 -0
  18. package/build/next/index.js +44 -0
  19. package/build/next/native-module.d.ts +78 -0
  20. package/build/next/native-module.js +2 -0
  21. package/build/next/query.d.ts +13 -0
  22. package/build/next/query.js +16 -0
  23. package/build/next/types.d.ts +92 -0
  24. package/build/next/types.js +30 -0
  25. package/build-ngc/angular/index.d.ts +1 -0
  26. package/build-ngc/angular/index.js +2 -0
  27. package/build-ngc/angular/index.js.map +1 -0
  28. package/build-ngc/next/album.d.ts +9 -0
  29. package/build-ngc/next/album.js +13 -0
  30. package/build-ngc/next/album.js.map +1 -0
  31. package/build-ngc/next/asset.d.ts +13 -0
  32. package/build-ngc/next/asset.js +35 -0
  33. package/build-ngc/next/asset.js.map +1 -0
  34. package/build-ngc/next/index.d.ts +24 -0
  35. package/build-ngc/next/index.js +45 -0
  36. package/build-ngc/next/index.js.map +1 -0
  37. package/build-ngc/next/native-module.d.ts +78 -0
  38. package/build-ngc/next/native-module.js +3 -0
  39. package/build-ngc/next/native-module.js.map +1 -0
  40. package/build-ngc/next/query.d.ts +13 -0
  41. package/build-ngc/next/query.js +17 -0
  42. package/build-ngc/next/query.js.map +1 -0
  43. package/build-ngc/next/types.d.ts +92 -0
  44. package/build-ngc/next/types.js +31 -0
  45. package/build-ngc/next/types.js.map +1 -0
  46. package/native-link.json +34 -0
  47. package/package.json +154 -0
  48. package/src/angular/index.ts +1 -0
  49. package/src/core/index.ts +7 -0
  50. package/src/core/media-library.ts +443 -0
  51. package/src/core/native-module.ts +80 -0
  52. package/src/core/types.ts +163 -0
  53. package/src/next/album.ts +13 -0
  54. package/src/next/asset.ts +51 -0
  55. package/src/next/index.ts +82 -0
  56. package/src/next/native-module.ts +116 -0
  57. package/src/next/query.ts +17 -0
  58. package/src/next/types.ts +106 -0
package/package.json ADDED
@@ -0,0 +1,154 @@
1
+ {
2
+ "name": "@symbiote-native/media-library",
3
+ "version": "0.1.0",
4
+ "description": "expo-media-library wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, and Angular adapters. Matches upstream's own layout: the modern shared-object Query/Asset/Album API at the package root, and the legacy function-based API at the ./legacy subpath. Reads, saves and organizes the device's photo/video library (assets, albums, permissions, library-change events).",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/OneEyed1366/symbiote-native.git",
9
+ "directory": "packages/media-library"
10
+ },
11
+ "homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/media-library#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/OneEyed1366/symbiote-native/issues"
14
+ },
15
+ "author": "Andrey Prokopenko <psevdoproger@gmail.com>",
16
+ "keywords": [
17
+ "react-native",
18
+ "symbiote-native",
19
+ "expo-media-library",
20
+ "react",
21
+ "vue",
22
+ "svelte",
23
+ "solid",
24
+ "angular",
25
+ "media-library",
26
+ "camera-roll"
27
+ ],
28
+ "type": "module",
29
+ "main": "./build/next/index.js",
30
+ "module": "./build/next/index.js",
31
+ "types": "./build/next/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./build/next/index.d.ts",
35
+ "default": "./build/next/index.js"
36
+ },
37
+ "./vue": {
38
+ "types": "./build/next/index.d.ts",
39
+ "default": "./build/next/index.js"
40
+ },
41
+ "./react": {
42
+ "types": "./build/next/index.d.ts",
43
+ "default": "./build/next/index.js"
44
+ },
45
+ "./svelte": {
46
+ "types": "./build/next/index.d.ts",
47
+ "default": "./build/next/index.js"
48
+ },
49
+ "./solid": {
50
+ "types": "./build/next/index.d.ts",
51
+ "default": "./build/next/index.js"
52
+ },
53
+ "./angular": {
54
+ "types": "./build-ngc/angular/index.d.ts",
55
+ "react-native": "./build-ngc/angular/index.js",
56
+ "default": "./src/angular/index.ts"
57
+ },
58
+ "./legacy": {
59
+ "types": "./build/core/index.d.ts",
60
+ "default": "./build/core/index.js"
61
+ }
62
+ },
63
+ "files": [
64
+ "src",
65
+ "build",
66
+ "build-ngc",
67
+ "native-link.json",
68
+ "!src/**/*.test.*",
69
+ "!src/**/*.spec.*",
70
+ "!src/**/*.detox.*"
71
+ ],
72
+ "publishConfig": {
73
+ "access": "public"
74
+ },
75
+ "dependencies": {
76
+ "expo-media-library": "57.0.4",
77
+ "expo-modules-core": "57.0.5"
78
+ },
79
+ "peerDependencies": {
80
+ "@angular/core": ">=20",
81
+ "@vue/runtime-core": "^3.5.13",
82
+ "react": ">=19.0.0",
83
+ "react-native": ">=0.86",
84
+ "solid-js": ">=1.9.0",
85
+ "svelte": ">=5.56.0",
86
+ "vue": ">=3.5.0",
87
+ "@symbiote-native/angular": "^3.1.0",
88
+ "@symbiote-native/solid": "^3.0.2",
89
+ "@symbiote-native/react": "^3.0.2",
90
+ "@symbiote-native/engine": "^1.3.0",
91
+ "@symbiote-native/vue": "^3.0.2",
92
+ "@symbiote-native/svelte": "^3.0.2"
93
+ },
94
+ "peerDependenciesMeta": {
95
+ "@symbiote-native/angular": {
96
+ "optional": true
97
+ },
98
+ "@symbiote-native/react": {
99
+ "optional": true
100
+ },
101
+ "@symbiote-native/solid": {
102
+ "optional": true
103
+ },
104
+ "@symbiote-native/svelte": {
105
+ "optional": true
106
+ },
107
+ "@symbiote-native/vue": {
108
+ "optional": true
109
+ },
110
+ "@angular/core": {
111
+ "optional": true
112
+ },
113
+ "@vue/runtime-core": {
114
+ "optional": true
115
+ },
116
+ "react": {
117
+ "optional": true
118
+ },
119
+ "solid-js": {
120
+ "optional": true
121
+ },
122
+ "svelte": {
123
+ "optional": true
124
+ },
125
+ "vue": {
126
+ "optional": true
127
+ }
128
+ },
129
+ "devDependencies": {
130
+ "@angular/compiler": "~22.0.8",
131
+ "@angular/compiler-cli": "~22.0.8",
132
+ "@angular/core": "~22.0.8",
133
+ "@types/node": "^26.0.0",
134
+ "@types/react": "^19.2.0",
135
+ "@vue/runtime-core": "^3.5.13",
136
+ "react": "19.2.3",
137
+ "solid-js": "^1.9.14",
138
+ "svelte": "^5.56.0",
139
+ "typescript": "~6.0.0",
140
+ "@symbiote-native/angular": "3.1.0",
141
+ "@symbiote-native/engine": "1.3.0",
142
+ "@symbiote-native/solid": "3.0.2",
143
+ "@symbiote-native/svelte": "3.0.2",
144
+ "@symbiote-native/test-utils": "0.4.2",
145
+ "@symbiote-native/vue": "3.0.2",
146
+ "@symbiote-native/react": "3.0.2"
147
+ },
148
+ "scripts": {
149
+ "typecheck": "tsc --build",
150
+ "clean": "rm -rf build-ngc",
151
+ "ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
152
+ "format": "prettier --write \"src/**/*.{ts,tsx}\""
153
+ }
154
+ }
@@ -0,0 +1 @@
1
+ export * from '../next';
@@ -0,0 +1,7 @@
1
+ export {
2
+ PermissionStatus,
3
+ type PermissionExpiration,
4
+ type PermissionHookOptions,
5
+ } from 'expo-modules-core';
6
+ export * from './media-library';
7
+ export * from './types';
@@ -0,0 +1,443 @@
1
+ import { createPermissionHook } from 'expo-modules-core';
2
+ import { Platform } from 'react-native';
3
+ import { expoMediaLibrary } from './native-module';
4
+ import type {
5
+ IGranularPermission,
6
+ IMediaLibraryAlbum,
7
+ IMediaLibraryAlbumRef,
8
+ IMediaLibraryAlbumsOptions,
9
+ IMediaLibraryAsset,
10
+ IMediaLibraryAssetInfo,
11
+ IMediaLibraryAssetInfoQueryOptions,
12
+ IMediaLibraryAssetRef,
13
+ IMediaLibraryAssetsChangeEvent,
14
+ IMediaLibraryAssetsOptions,
15
+ IMediaLibraryMediaTypeObject,
16
+ IMediaLibraryMediaTypeFilter,
17
+ IMediaLibraryPagedInfo,
18
+ IMediaLibraryPermissionResponse,
19
+ IMediaLibrarySortByObject,
20
+ IMediaLibrarySortByValue,
21
+ } from './types';
22
+
23
+ type IInternalSortByValue = `${string} ${'ASC' | 'DESC'}`;
24
+
25
+ function arrayize<T>(item: T | T[] | undefined): T[] {
26
+ if (Array.isArray(item)) return item;
27
+ return item ? [item] : [];
28
+ }
29
+
30
+ function getId(ref: string | undefined | { id?: string }): string | undefined {
31
+ if (typeof ref === 'string') return ref;
32
+ return ref ? ref.id : undefined;
33
+ }
34
+
35
+ function assertAssetIds(assetIds: unknown[]): asserts assetIds is string[] {
36
+ if (assetIds.some(id => !id || typeof id !== 'string')) {
37
+ throw new Error('Asset ID must be a string!');
38
+ }
39
+ }
40
+
41
+ function assertAlbumIds(albumIds: unknown[]): asserts albumIds is string[] {
42
+ if (albumIds.some(id => !id || typeof id !== 'string')) {
43
+ throw new Error('Album ID must be a string!');
44
+ }
45
+ }
46
+
47
+ function assertMediaType(
48
+ mediaType: unknown,
49
+ ): asserts mediaType is keyof IMediaLibraryMediaTypeObject {
50
+ if (!(Object.values(MediaType) as string[]).includes(mediaType as string)) {
51
+ throw new Error(`Invalid mediaType: ${String(mediaType)}`);
52
+ }
53
+ }
54
+
55
+ function assertSortByKey(sortBy: unknown): void {
56
+ if (!(Object.values(SortBy) as string[]).includes(sortBy as string)) {
57
+ throw new Error(`Invalid sortBy key: ${String(sortBy)}`);
58
+ }
59
+ }
60
+
61
+ function assertSortBy(
62
+ sortBy: unknown,
63
+ ): asserts sortBy is IMediaLibrarySortByValue {
64
+ if (Array.isArray(sortBy)) {
65
+ assertSortByKey(sortBy[0]);
66
+ if (typeof sortBy[1] !== 'boolean') {
67
+ throw new Error(
68
+ 'Invalid sortBy array argument. Second item must be a boolean!',
69
+ );
70
+ }
71
+ } else {
72
+ assertSortByKey(sortBy);
73
+ }
74
+ }
75
+
76
+ function sortByOptionToString(
77
+ sortBy: IMediaLibrarySortByValue,
78
+ ): IInternalSortByValue {
79
+ assertSortBy(sortBy);
80
+ if (Array.isArray(sortBy)) {
81
+ return `${sortBy[0]} ${sortBy[1] ? 'ASC' : 'DESC'}`;
82
+ }
83
+ return `${sortBy} DESC`;
84
+ }
85
+
86
+ function dateToNumber(value?: Date | number): number | undefined {
87
+ return value instanceof Date ? value.getTime() : value;
88
+ }
89
+
90
+ export const MediaType: IMediaLibraryMediaTypeObject =
91
+ expoMediaLibrary.MediaType;
92
+ export const SortBy: IMediaLibrarySortByObject = expoMediaLibrary.SortBy;
93
+
94
+ export async function isAvailableAsync(): Promise<boolean> {
95
+ return !!expoMediaLibrary && 'getAssetsAsync' in expoMediaLibrary;
96
+ }
97
+
98
+ /**
99
+ * @param granularPermissions Only has an effect on Android 13+. Defaults to every possible
100
+ * permission — make sure your app manifest requests everything you pass here.
101
+ */
102
+ export async function requestPermissionsAsync(
103
+ writeOnly: boolean = false,
104
+ granularPermissions?: IGranularPermission[],
105
+ ): Promise<IMediaLibraryPermissionResponse> {
106
+ if (Platform.OS === 'android') {
107
+ return expoMediaLibrary.requestPermissionsAsync(
108
+ writeOnly,
109
+ granularPermissions,
110
+ );
111
+ }
112
+ return expoMediaLibrary.requestPermissionsAsync(writeOnly);
113
+ }
114
+
115
+ export async function getPermissionsAsync(
116
+ writeOnly: boolean = false,
117
+ granularPermissions?: IGranularPermission[],
118
+ ): Promise<IMediaLibraryPermissionResponse> {
119
+ if (Platform.OS === 'android') {
120
+ return expoMediaLibrary.getPermissionsAsync(writeOnly, granularPermissions);
121
+ }
122
+ return expoMediaLibrary.getPermissionsAsync(writeOnly);
123
+ }
124
+
125
+ type IMediaLibraryPermissionHookOptions = {
126
+ writeOnly?: boolean;
127
+ granularPermissions?: IGranularPermission[];
128
+ };
129
+
130
+ export const usePermissions = createPermissionHook<
131
+ IMediaLibraryPermissionResponse,
132
+ IMediaLibraryPermissionHookOptions
133
+ >({
134
+ getMethod: (options?: IMediaLibraryPermissionHookOptions) =>
135
+ getPermissionsAsync(options?.writeOnly, options?.granularPermissions),
136
+ requestMethod: (options?: IMediaLibraryPermissionHookOptions) =>
137
+ requestPermissionsAsync(options?.writeOnly, options?.granularPermissions),
138
+ });
139
+
140
+ /**
141
+ * Lets the user update which assets your app has access to. Only a no-op unless the user
142
+ * previously granted `'limited'` access.
143
+ * @platform android 14+
144
+ * @platform ios
145
+ */
146
+ export async function presentPermissionsPickerAsync(
147
+ mediaTypes: IMediaLibraryMediaTypeFilter[] = ['photo', 'video'],
148
+ ): Promise<void> {
149
+ if (Platform.OS === 'android' && Platform.Version >= 34) {
150
+ await expoMediaLibrary.requestPermissionsAsync(
151
+ false,
152
+ mediaTypes as unknown as IGranularPermission[],
153
+ );
154
+ return;
155
+ }
156
+ if (!expoMediaLibrary.presentPermissionsPickerAsync) {
157
+ throw new Error(
158
+ 'presentPermissionsPickerAsync is not available on this platform.',
159
+ );
160
+ }
161
+ return expoMediaLibrary.presentPermissionsPickerAsync();
162
+ }
163
+
164
+ /**
165
+ * Creates an asset from an existing local file — the common case is a picture just taken by a
166
+ * camera. Requires write permission.
167
+ * @param localUri Must contain an extension. On Android it must be a local path (`file:///…`).
168
+ * @param album If provided, the asset is added to this album on creation.
169
+ */
170
+ export async function createAssetAsync(
171
+ localUri: string,
172
+ album?: IMediaLibraryAlbumRef,
173
+ ): Promise<IMediaLibraryAsset> {
174
+ if (!localUri || typeof localUri !== 'string') {
175
+ throw new Error('Invalid argument "localUri". It must be a string!');
176
+ }
177
+ const asset = await expoMediaLibrary.createAssetAsync(localUri, getId(album));
178
+ return Array.isArray(asset) ? asset[0] : asset;
179
+ }
180
+
181
+ /**
182
+ * Saves the file at `localUri` to the user's library. Unlike `createAssetAsync`, does not
183
+ * return the created asset. On iOS 11+ this can be called without the read/write permission as
184
+ * long as `Info.plist` carries `NSPhotoLibraryAddUsageDescription`.
185
+ */
186
+ export async function saveToLibraryAsync(localUri: string): Promise<void> {
187
+ return expoMediaLibrary.saveToLibraryAsync(localUri);
188
+ }
189
+
190
+ /**
191
+ * On Android, copies assets from their current album into the given one by default — pass
192
+ * `copy: false` to move them instead. Copied assets show up twice in `getAssetsAsync`.
193
+ */
194
+ export async function addAssetsToAlbumAsync(
195
+ assets: IMediaLibraryAssetRef[] | IMediaLibraryAssetRef,
196
+ album: IMediaLibraryAlbumRef,
197
+ copy: boolean = true,
198
+ ): Promise<boolean> {
199
+ const assetIds = arrayize(assets).map(getId);
200
+ const albumId = getId(album);
201
+ assertAssetIds(assetIds);
202
+ if (!albumId || typeof albumId !== 'string') {
203
+ throw new Error('Invalid album ID. It must be a string!');
204
+ }
205
+ if (Platform.OS === 'ios') {
206
+ return expoMediaLibrary.addAssetsToAlbumAsync(assetIds, albumId);
207
+ }
208
+ return expoMediaLibrary.addAssetsToAlbumAsync(assetIds, albumId, !!copy);
209
+ }
210
+
211
+ /** On Android, an empty album is deleted automatically once its last asset is removed. */
212
+ export async function removeAssetsFromAlbumAsync(
213
+ assets: IMediaLibraryAssetRef[] | IMediaLibraryAssetRef,
214
+ album: IMediaLibraryAlbumRef,
215
+ ): Promise<boolean> {
216
+ const assetIds = arrayize(assets).map(getId);
217
+ assertAssetIds(assetIds);
218
+ return expoMediaLibrary.removeAssetsFromAlbumAsync(assetIds, getId(album));
219
+ }
220
+
221
+ /**
222
+ * iOS removes the asset from every album it belongs to (behind a system confirmation dialog);
223
+ * Android keeps other albums' copies (an asset is strictly tied to one album there).
224
+ */
225
+ export async function deleteAssetsAsync(
226
+ assets: IMediaLibraryAssetRef[] | IMediaLibraryAssetRef,
227
+ ): Promise<boolean> {
228
+ const assetIds = arrayize(assets).map(getId);
229
+ assertAssetIds(assetIds);
230
+ return expoMediaLibrary.deleteAssetsAsync(assetIds);
231
+ }
232
+
233
+ /**
234
+ * GPS location, local URI and EXIF metadata. Prefer this only when you need more than the
235
+ * fields already on `Asset` — it costs more than a plain list query.
236
+ */
237
+ export async function getAssetInfoAsync(
238
+ asset: IMediaLibraryAssetRef,
239
+ options: IMediaLibraryAssetInfoQueryOptions = {
240
+ shouldDownloadFromNetwork: true,
241
+ },
242
+ ): Promise<IMediaLibraryAssetInfo> {
243
+ const assetId = getId(asset);
244
+ assertAssetIds([assetId]);
245
+ const info = await expoMediaLibrary.getAssetInfoAsync(
246
+ assetId as string,
247
+ options,
248
+ );
249
+ return Array.isArray(info) ? info[0] : info;
250
+ }
251
+
252
+ /** @platform android */
253
+ export async function getAssetContentUriAsync(
254
+ asset: IMediaLibraryAssetRef,
255
+ ): Promise<string> {
256
+ if (Platform.OS !== 'android') {
257
+ throw new Error('getAssetContentUriAsync is only available on Android.');
258
+ }
259
+ const assetId = getId(asset);
260
+ assertAssetIds([assetId]);
261
+ return expoMediaLibrary.getAssetContentUriAsync(assetId as string);
262
+ }
263
+
264
+ /**
265
+ * @return Depending on Android version, the root storage directory may be listed as an album
266
+ * titled `"0"`, or not listed at all.
267
+ */
268
+ export async function getAlbumsAsync({
269
+ includeSmartAlbums = false,
270
+ }: IMediaLibraryAlbumsOptions = {}): Promise<IMediaLibraryAlbum[]> {
271
+ return expoMediaLibrary.getAlbumsAsync({ includeSmartAlbums });
272
+ }
273
+
274
+ export async function getAlbumAsync(
275
+ title: string,
276
+ ): Promise<IMediaLibraryAlbum> {
277
+ if (typeof title !== 'string') {
278
+ throw new Error('Album title must be a string!');
279
+ }
280
+ return expoMediaLibrary.getAlbumAsync(title);
281
+ }
282
+
283
+ /**
284
+ * `asset` (or `initialAssetLocalUri`) is required on Android — an empty album cannot be
285
+ * created on that platform.
286
+ */
287
+ export async function createAlbumAsync(
288
+ albumName: string,
289
+ asset?: IMediaLibraryAssetRef,
290
+ copyAsset: boolean = true,
291
+ initialAssetLocalUri?: string,
292
+ ): Promise<IMediaLibraryAlbum> {
293
+ const assetId = getId(asset);
294
+
295
+ if (
296
+ Platform.OS === 'android' &&
297
+ (typeof assetId !== 'string' || assetId.length === 0) &&
298
+ !initialAssetLocalUri
299
+ ) {
300
+ throw new Error(
301
+ 'createAlbumAsync must be called with an asset or a localUri on Android.',
302
+ );
303
+ }
304
+ if (!albumName || typeof albumName !== 'string') {
305
+ throw new Error('Invalid argument "albumName". It must be a string!');
306
+ }
307
+
308
+ if (Platform.OS === 'ios') {
309
+ return expoMediaLibrary.createAlbumAsync(
310
+ albumName,
311
+ assetId,
312
+ initialAssetLocalUri,
313
+ );
314
+ }
315
+ return expoMediaLibrary.createAlbumAsync(
316
+ albumName,
317
+ assetId,
318
+ !!copyAsset,
319
+ initialAssetLocalUri,
320
+ );
321
+ }
322
+
323
+ /**
324
+ * iOS keeps the assets in the main library unless `deleteAssets` is set; Android always drops
325
+ * an album's assets along with it.
326
+ */
327
+ export async function deleteAlbumsAsync(
328
+ albums: IMediaLibraryAlbumRef[] | IMediaLibraryAlbumRef,
329
+ deleteAssets: boolean = false,
330
+ ): Promise<boolean> {
331
+ const albumIds = arrayize(albums).map(getId);
332
+ assertAlbumIds(albumIds);
333
+ if (Platform.OS === 'android') {
334
+ return expoMediaLibrary.deleteAlbumsAsync(albumIds);
335
+ }
336
+ return expoMediaLibrary.deleteAlbumsAsync(albumIds, !!deleteAssets);
337
+ }
338
+
339
+ // UPSTREAM-BUG(expo): legacy/MediaLibrary.ts's own `arrayize()` passes any array through
340
+ // unchanged, so a single `[key, ascending]` tuple in `sortBy` is misread as two independent
341
+ // sort keys instead of one pair — the caller must double-nest it (`[[key, ascending]]`) to
342
+ // sort by one key ascending. Ported verbatim for parity; see media-library.test.ts.
343
+ export async function getAssetsAsync(
344
+ assetsOptions: IMediaLibraryAssetsOptions = {},
345
+ ): Promise<IMediaLibraryPagedInfo<IMediaLibraryAsset>> {
346
+ const {
347
+ first,
348
+ after,
349
+ album,
350
+ sortBy,
351
+ mediaType,
352
+ createdAfter,
353
+ createdBefore,
354
+ mediaSubtypes,
355
+ resolveWithFullInfo,
356
+ } = assetsOptions;
357
+
358
+ if (first != null && first < 0) {
359
+ throw new Error('Option "first" must be a positive integer!');
360
+ }
361
+ const afterId = getId(after);
362
+ if (
363
+ after != null &&
364
+ Platform.OS === 'android' &&
365
+ isNaN(parseInt(afterId as string, 10))
366
+ ) {
367
+ throw new Error('Option "after" must be a valid ID!');
368
+ }
369
+
370
+ const mediaTypeList = arrayize(mediaType || [MediaType.photo]);
371
+ mediaTypeList.forEach(assertMediaType);
372
+
373
+ return expoMediaLibrary.getAssetsAsync({
374
+ first: first == null ? 20 : first,
375
+ after: afterId,
376
+ album: getId(album),
377
+ sortBy: arrayize(sortBy).map(sortByOptionToString),
378
+ mediaType: mediaTypeList,
379
+ mediaSubtypes: arrayize(mediaSubtypes),
380
+ createdAfter: dateToNumber(createdAfter),
381
+ createdBefore: dateToNumber(createdBefore),
382
+ resolveWithFullInfo: resolveWithFullInfo ?? false,
383
+ });
384
+ }
385
+
386
+ /**
387
+ * @param listener Fired on Android with an empty object; on iOS with a real
388
+ * `MediaLibraryAssetsChangeEvent`, and also whenever the user updates per-asset access via
389
+ * `presentPermissionsPickerAsync()`.
390
+ */
391
+ export function addListener(
392
+ listener: (event: IMediaLibraryAssetsChangeEvent) => void,
393
+ ) {
394
+ return expoMediaLibrary.addListener(
395
+ expoMediaLibrary.CHANGE_LISTENER_NAME,
396
+ (event: unknown) => listener(event as IMediaLibraryAssetsChangeEvent),
397
+ );
398
+ }
399
+
400
+ export function removeAllListeners(): void {
401
+ expoMediaLibrary.removeAllListeners(expoMediaLibrary.CHANGE_LISTENER_NAME);
402
+ }
403
+
404
+ /**
405
+ * A "moment" is a group of assets taken around the same place and time.
406
+ * @platform ios
407
+ */
408
+ export async function getMomentsAsync(): Promise<IMediaLibraryAlbum[]> {
409
+ return expoMediaLibrary.getMomentsAsync();
410
+ }
411
+
412
+ /**
413
+ * Moves an album's content into the scoped-storage media directories, only where needed
414
+ * (Android R+, and only if the app lacks write permission to the album folder). No-op on iOS,
415
+ * web, and Android below R, or if the app already has write access.
416
+ */
417
+ export async function migrateAlbumIfNeededAsync(
418
+ album: IMediaLibraryAlbumRef,
419
+ ): Promise<void> {
420
+ if (!expoMediaLibrary.migrateAlbumIfNeededAsync) return;
421
+ return expoMediaLibrary.migrateAlbumIfNeededAsync(getId(album));
422
+ }
423
+
424
+ /** Always `false` on iOS, web, and Android below R. */
425
+ export async function albumNeedsMigrationAsync(
426
+ album: IMediaLibraryAlbumRef,
427
+ ): Promise<boolean> {
428
+ if (!expoMediaLibrary.albumNeedsMigrationAsync) return false;
429
+ return expoMediaLibrary.albumNeedsMigrationAsync(getId(album));
430
+ }
431
+
432
+ /** Adds or removes the asset from the system "Favorites" smart album. @platform ios */
433
+ export async function setAssetFavoriteAsync(
434
+ asset: IMediaLibraryAssetRef,
435
+ isFavorite: boolean,
436
+ ): Promise<boolean> {
437
+ if (Platform.OS !== 'ios') {
438
+ throw new Error('setAssetFavoriteAsync is only available on iOS.');
439
+ }
440
+ const assetId = getId(asset);
441
+ assertAssetIds([assetId]);
442
+ return expoMediaLibrary.setAssetFavoriteAsync(assetId as string, isFavorite);
443
+ }
@@ -0,0 +1,80 @@
1
+ import { requireNativeModule, type EventSubscription } from 'expo-modules-core';
2
+ import type {
3
+ IGranularPermission,
4
+ IMediaLibraryAlbum,
5
+ IMediaLibraryAlbumsOptions,
6
+ IMediaLibraryAsset,
7
+ IMediaLibraryAssetInfo,
8
+ IMediaLibraryAssetInfoQueryOptions,
9
+ IMediaLibraryMediaTypeObject,
10
+ IMediaLibraryPagedInfo,
11
+ IMediaLibraryPermissionResponse,
12
+ IMediaLibrarySortByObject,
13
+ } from './types';
14
+
15
+ const EXPO_MEDIA_LIBRARY_MODULE_NAME = 'ExpoMediaLibrary';
16
+
17
+ export type INativeMediaLibraryModule = {
18
+ MediaType: IMediaLibraryMediaTypeObject;
19
+ SortBy: IMediaLibrarySortByObject;
20
+ CHANGE_LISTENER_NAME: string;
21
+ addListener(
22
+ eventName: string,
23
+ listener: (event: unknown) => void,
24
+ ): EventSubscription;
25
+ removeAllListeners(eventName: string): void;
26
+ requestPermissionsAsync(
27
+ writeOnly: boolean,
28
+ granularPermissions?: IGranularPermission[],
29
+ ): Promise<IMediaLibraryPermissionResponse>;
30
+ getPermissionsAsync(
31
+ writeOnly: boolean,
32
+ granularPermissions?: IGranularPermission[],
33
+ ): Promise<IMediaLibraryPermissionResponse>;
34
+ presentPermissionsPickerAsync?(): Promise<void>;
35
+ createAssetAsync(
36
+ localUri: string,
37
+ albumId?: string,
38
+ ): Promise<IMediaLibraryAsset | IMediaLibraryAsset[]>;
39
+ saveToLibraryAsync(localUri: string): Promise<void>;
40
+ addAssetsToAlbumAsync(
41
+ assetIds: string[],
42
+ albumId: string,
43
+ copy?: boolean,
44
+ ): Promise<boolean>;
45
+ removeAssetsFromAlbumAsync(
46
+ assetIds: string[],
47
+ albumId: string | undefined,
48
+ ): Promise<boolean>;
49
+ deleteAssetsAsync(assetIds: string[]): Promise<boolean>;
50
+ getAssetInfoAsync(
51
+ assetId: string,
52
+ options: IMediaLibraryAssetInfoQueryOptions,
53
+ ): Promise<IMediaLibraryAssetInfo | IMediaLibraryAssetInfo[]>;
54
+ getAssetContentUriAsync(assetId: string): Promise<string>;
55
+ getAlbumsAsync(
56
+ options: IMediaLibraryAlbumsOptions,
57
+ ): Promise<IMediaLibraryAlbum[]>;
58
+ getAlbumAsync(title: string): Promise<IMediaLibraryAlbum>;
59
+ createAlbumAsync(
60
+ albumName: string,
61
+ assetId: string | undefined,
62
+ copyOrInitialUri?: boolean | string,
63
+ initialAssetLocalUri?: string,
64
+ ): Promise<IMediaLibraryAlbum>;
65
+ deleteAlbumsAsync(
66
+ albumIds: string[],
67
+ deleteAssets?: boolean,
68
+ ): Promise<boolean>;
69
+ getAssetsAsync(
70
+ options: Record<string, unknown>,
71
+ ): Promise<IMediaLibraryPagedInfo<IMediaLibraryAsset>>;
72
+ getMomentsAsync(): Promise<IMediaLibraryAlbum[]>;
73
+ migrateAlbumIfNeededAsync?(albumId: string | undefined): Promise<void>;
74
+ albumNeedsMigrationAsync?(albumId: string | undefined): Promise<boolean>;
75
+ setAssetFavoriteAsync(assetId: string, isFavorite: boolean): Promise<boolean>;
76
+ };
77
+
78
+ export const expoMediaLibrary = requireNativeModule<INativeMediaLibraryModule>(
79
+ EXPO_MEDIA_LIBRARY_MODULE_NAME,
80
+ );