@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.
- package/LICENSE +21 -0
- package/README.md +284 -0
- package/build/angular/index.d.ts +1 -0
- package/build/angular/index.js +1 -0
- package/build/core/index.d.ts +3 -0
- package/build/core/index.js +3 -0
- package/build/core/media-library.d.ts +94 -0
- package/build/core/media-library.js +284 -0
- package/build/core/native-module.d.ts +29 -0
- package/build/core/native-module.js +3 -0
- package/build/core/types.d.ts +126 -0
- package/build/core/types.js +1 -0
- package/build/next/album.d.ts +9 -0
- package/build/next/album.js +12 -0
- package/build/next/asset.d.ts +13 -0
- package/build/next/asset.js +34 -0
- package/build/next/index.d.ts +24 -0
- package/build/next/index.js +44 -0
- package/build/next/native-module.d.ts +78 -0
- package/build/next/native-module.js +2 -0
- package/build/next/query.d.ts +13 -0
- package/build/next/query.js +16 -0
- package/build/next/types.d.ts +92 -0
- package/build/next/types.js +30 -0
- package/build-ngc/angular/index.d.ts +1 -0
- package/build-ngc/angular/index.js +2 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/next/album.d.ts +9 -0
- package/build-ngc/next/album.js +13 -0
- package/build-ngc/next/album.js.map +1 -0
- package/build-ngc/next/asset.d.ts +13 -0
- package/build-ngc/next/asset.js +35 -0
- package/build-ngc/next/asset.js.map +1 -0
- package/build-ngc/next/index.d.ts +24 -0
- package/build-ngc/next/index.js +45 -0
- package/build-ngc/next/index.js.map +1 -0
- package/build-ngc/next/native-module.d.ts +78 -0
- package/build-ngc/next/native-module.js +3 -0
- package/build-ngc/next/native-module.js.map +1 -0
- package/build-ngc/next/query.d.ts +13 -0
- package/build-ngc/next/query.js +17 -0
- package/build-ngc/next/query.js.map +1 -0
- package/build-ngc/next/types.d.ts +92 -0
- package/build-ngc/next/types.js +31 -0
- package/build-ngc/next/types.js.map +1 -0
- package/native-link.json +34 -0
- package/package.json +154 -0
- package/src/angular/index.ts +1 -0
- package/src/core/index.ts +7 -0
- package/src/core/media-library.ts +443 -0
- package/src/core/native-module.ts +80 -0
- package/src/core/types.ts +163 -0
- package/src/next/album.ts +13 -0
- package/src/next/asset.ts +51 -0
- package/src/next/index.ts +82 -0
- package/src/next/native-module.ts +116 -0
- package/src/next/query.ts +17 -0
- package/src/next/types.ts +106 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expoMediaLibraryNext } from './native-module';
|
|
2
|
+
import { Asset } from './asset';
|
|
3
|
+
/**
|
|
4
|
+
* A query builder over the media library, matching upstream's own trivial subclass
|
|
5
|
+
* (`export class Query extends ExpoMediaLibraryNext.Query {}`) for every method but `exe()`:
|
|
6
|
+
* the native call constructs its results off the BASE native asset class, never our own `Asset`
|
|
7
|
+
* (asset.ts) — so without this override, a query result's iOS-only getters silently skip the
|
|
8
|
+
* platform guard every other `Asset` gets. Re-wrapping by id is cheap (a shared-object handle,
|
|
9
|
+
* not a data copy) and keeps `exe()` consistent with every other way of obtaining an `Asset`.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Query extends expoMediaLibraryNext.Query {
|
|
12
|
+
exe(): Promise<Asset[]>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expoMediaLibraryNext } from './native-module.js';
|
|
2
|
+
import { Asset } from './asset.js';
|
|
3
|
+
/**
|
|
4
|
+
* A query builder over the media library, matching upstream's own trivial subclass
|
|
5
|
+
* (`export class Query extends ExpoMediaLibraryNext.Query {}`) for every method but `exe()`:
|
|
6
|
+
* the native call constructs its results off the BASE native asset class, never our own `Asset`
|
|
7
|
+
* (asset.ts) — so without this override, a query result's iOS-only getters silently skip the
|
|
8
|
+
* platform guard every other `Asset` gets. Re-wrapping by id is cheap (a shared-object handle,
|
|
9
|
+
* not a data copy) and keeps `exe()` consistent with every other way of obtaining an `Asset`.
|
|
10
|
+
*/
|
|
11
|
+
export class Query extends expoMediaLibraryNext.Query {
|
|
12
|
+
async exe() {
|
|
13
|
+
const results = await super.exe();
|
|
14
|
+
return results.map(result => new Asset(result.id));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { PermissionResponse } from 'expo-modules-core';
|
|
2
|
+
export declare enum AssetField {
|
|
3
|
+
CREATION_TIME = "creationTime",
|
|
4
|
+
MODIFICATION_TIME = "modificationTime",
|
|
5
|
+
MEDIA_TYPE = "mediaType",
|
|
6
|
+
WIDTH = "width",
|
|
7
|
+
HEIGHT = "height",
|
|
8
|
+
DURATION = "duration",
|
|
9
|
+
IS_FAVORITE = "isFavorite"
|
|
10
|
+
}
|
|
11
|
+
export declare enum MediaType {
|
|
12
|
+
UNKNOWN = "unknown",
|
|
13
|
+
IMAGE = "image",
|
|
14
|
+
AUDIO = "audio",
|
|
15
|
+
VIDEO = "video"
|
|
16
|
+
}
|
|
17
|
+
export declare enum MediaSubtype {
|
|
18
|
+
DEPTH_EFFECT = "depthEffect",
|
|
19
|
+
HDR = "hdr",
|
|
20
|
+
HIGH_FRAME_RATE = "highFrameRate",
|
|
21
|
+
LIVE_PHOTO = "livePhoto",
|
|
22
|
+
PANORAMA = "panorama",
|
|
23
|
+
SCREENSHOT = "screenshot",
|
|
24
|
+
STREAM = "stream",
|
|
25
|
+
TIME_LAPSE = "timelapse",
|
|
26
|
+
SPATIAL_MEDIA = "spatialMedia",
|
|
27
|
+
VIDEO_CINEMATIC = "videoCinematic"
|
|
28
|
+
}
|
|
29
|
+
export type IAssetFieldValueMap = {
|
|
30
|
+
[AssetField.CREATION_TIME]: number;
|
|
31
|
+
[AssetField.MODIFICATION_TIME]: number;
|
|
32
|
+
[AssetField.MEDIA_TYPE]: MediaType;
|
|
33
|
+
[AssetField.WIDTH]: number;
|
|
34
|
+
[AssetField.HEIGHT]: number;
|
|
35
|
+
[AssetField.DURATION]: number;
|
|
36
|
+
[AssetField.IS_FAVORITE]: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type ISortDescriptor = {
|
|
39
|
+
key: AssetField;
|
|
40
|
+
ascending?: boolean;
|
|
41
|
+
};
|
|
42
|
+
export type IShape = {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
};
|
|
46
|
+
export type ILocation = {
|
|
47
|
+
latitude: number;
|
|
48
|
+
longitude: number;
|
|
49
|
+
};
|
|
50
|
+
export type IAssetInfo = {
|
|
51
|
+
id: string;
|
|
52
|
+
filename: string;
|
|
53
|
+
uri: string;
|
|
54
|
+
mediaType: MediaType;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
duration: number | null;
|
|
58
|
+
creationTime: number | null;
|
|
59
|
+
modificationTime: number | null;
|
|
60
|
+
isFavorite: boolean;
|
|
61
|
+
};
|
|
62
|
+
export type IAssetMetadata = {
|
|
63
|
+
id: string;
|
|
64
|
+
filename: string | null;
|
|
65
|
+
mediaType: MediaType;
|
|
66
|
+
width: number | null;
|
|
67
|
+
height: number | null;
|
|
68
|
+
duration: number | null;
|
|
69
|
+
creationTime: number | null;
|
|
70
|
+
modificationTime: number | null;
|
|
71
|
+
isFavorite: boolean;
|
|
72
|
+
};
|
|
73
|
+
export type IGranularPermission = 'audio' | 'photo' | 'video';
|
|
74
|
+
export type IMediaTypeFilter = 'photo' | 'video';
|
|
75
|
+
export type IMediaLibraryAssetsChangeEvent = {
|
|
76
|
+
hasIncrementalChanges: boolean;
|
|
77
|
+
insertedAssets?: string[];
|
|
78
|
+
deletedAssets?: string[];
|
|
79
|
+
updatedAssets?: string[];
|
|
80
|
+
};
|
|
81
|
+
export type IPermissionHookOptions = {
|
|
82
|
+
writeOnly?: boolean;
|
|
83
|
+
granularPermissions?: IGranularPermission[];
|
|
84
|
+
};
|
|
85
|
+
export type IMediaLibraryNextPermissionResponse = PermissionResponse & {
|
|
86
|
+
/**
|
|
87
|
+
* - `'all'` — full library access
|
|
88
|
+
* - `'limited'` — only selected photos (Android 14+, iOS 14+)
|
|
89
|
+
* - `'none'` — denied or not yet granted
|
|
90
|
+
*/
|
|
91
|
+
accessPrivileges?: 'all' | 'limited' | 'none';
|
|
92
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export var AssetField;
|
|
2
|
+
(function (AssetField) {
|
|
3
|
+
AssetField["CREATION_TIME"] = "creationTime";
|
|
4
|
+
AssetField["MODIFICATION_TIME"] = "modificationTime";
|
|
5
|
+
AssetField["MEDIA_TYPE"] = "mediaType";
|
|
6
|
+
AssetField["WIDTH"] = "width";
|
|
7
|
+
AssetField["HEIGHT"] = "height";
|
|
8
|
+
AssetField["DURATION"] = "duration";
|
|
9
|
+
AssetField["IS_FAVORITE"] = "isFavorite";
|
|
10
|
+
})(AssetField || (AssetField = {}));
|
|
11
|
+
export var MediaType;
|
|
12
|
+
(function (MediaType) {
|
|
13
|
+
MediaType["UNKNOWN"] = "unknown";
|
|
14
|
+
MediaType["IMAGE"] = "image";
|
|
15
|
+
MediaType["AUDIO"] = "audio";
|
|
16
|
+
MediaType["VIDEO"] = "video";
|
|
17
|
+
})(MediaType || (MediaType = {}));
|
|
18
|
+
export var MediaSubtype;
|
|
19
|
+
(function (MediaSubtype) {
|
|
20
|
+
MediaSubtype["DEPTH_EFFECT"] = "depthEffect";
|
|
21
|
+
MediaSubtype["HDR"] = "hdr";
|
|
22
|
+
MediaSubtype["HIGH_FRAME_RATE"] = "highFrameRate";
|
|
23
|
+
MediaSubtype["LIVE_PHOTO"] = "livePhoto";
|
|
24
|
+
MediaSubtype["PANORAMA"] = "panorama";
|
|
25
|
+
MediaSubtype["SCREENSHOT"] = "screenshot";
|
|
26
|
+
MediaSubtype["STREAM"] = "stream";
|
|
27
|
+
MediaSubtype["TIME_LAPSE"] = "timelapse";
|
|
28
|
+
MediaSubtype["SPATIAL_MEDIA"] = "spatialMedia";
|
|
29
|
+
MediaSubtype["VIDEO_CINEMATIC"] = "videoCinematic";
|
|
30
|
+
})(MediaSubtype || (MediaSubtype = {}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../next';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { expoMediaLibraryNext } from './native-module';
|
|
2
|
+
import { Asset } from './asset';
|
|
3
|
+
/**
|
|
4
|
+
* A media album (collection of assets) on the device, matching upstream's own trivial subclass
|
|
5
|
+
* for every method but `getAssets()` — same reason as Query.exe(), see query.ts.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Album extends expoMediaLibraryNext.Album {
|
|
8
|
+
getAssets(): Promise<Asset[]>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expoMediaLibraryNext } from './native-module';
|
|
2
|
+
import { Asset } from './asset';
|
|
3
|
+
/**
|
|
4
|
+
* A media album (collection of assets) on the device, matching upstream's own trivial subclass
|
|
5
|
+
* for every method but `getAssets()` — same reason as Query.exe(), see query.ts.
|
|
6
|
+
*/
|
|
7
|
+
export class Album extends expoMediaLibraryNext.Album {
|
|
8
|
+
async getAssets() {
|
|
9
|
+
const results = await super.getAssets();
|
|
10
|
+
return results.map(result => new Asset(result.id));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=album.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"album.js","sourceRoot":"","sources":["../../src/next/album.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC;;;GAGG;AACH,MAAM,OAAO,KAAM,SAAQ,oBAAoB,CAAC,KAAK;IAC1C,KAAK,CAAC,SAAS;QACtB,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE,CAAC;QACxC,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACrD,CAAC;CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expoMediaLibraryNext } from './native-module';
|
|
2
|
+
import type { MediaSubtype } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* A single media asset on the device (image, video, or audio). Four getters are iOS-only on
|
|
5
|
+
* upstream's own native side; guarded here to fail with the same `UnavailabilityError` upstream
|
|
6
|
+
* throws rather than whatever the Android native method happens to do when called anyway.
|
|
7
|
+
*/
|
|
8
|
+
export declare class Asset extends expoMediaLibraryNext.Asset {
|
|
9
|
+
getMediaSubtypes(): Promise<MediaSubtype[]>;
|
|
10
|
+
getLivePhotoVideoUri(): Promise<string | null>;
|
|
11
|
+
getIsInCloud(): Promise<boolean>;
|
|
12
|
+
getOrientation(): Promise<number | null>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import { UnavailabilityError } from 'expo-modules-core';
|
|
3
|
+
import { expoMediaLibraryNext } from './native-module';
|
|
4
|
+
/**
|
|
5
|
+
* A single media asset on the device (image, video, or audio). Four getters are iOS-only on
|
|
6
|
+
* upstream's own native side; guarded here to fail with the same `UnavailabilityError` upstream
|
|
7
|
+
* throws rather than whatever the Android native method happens to do when called anyway.
|
|
8
|
+
*/
|
|
9
|
+
export class Asset extends expoMediaLibraryNext.Asset {
|
|
10
|
+
getMediaSubtypes() {
|
|
11
|
+
if (Platform.OS !== 'ios') {
|
|
12
|
+
throw new UnavailabilityError('MediaLibrary', 'getMediaSubtypes is only available on iOS');
|
|
13
|
+
}
|
|
14
|
+
return super.getMediaSubtypes();
|
|
15
|
+
}
|
|
16
|
+
getLivePhotoVideoUri() {
|
|
17
|
+
if (Platform.OS !== 'ios') {
|
|
18
|
+
throw new UnavailabilityError('MediaLibrary', 'getLivePhotoVideoUri is only available on iOS');
|
|
19
|
+
}
|
|
20
|
+
return super.getLivePhotoVideoUri();
|
|
21
|
+
}
|
|
22
|
+
getIsInCloud() {
|
|
23
|
+
if (Platform.OS !== 'ios') {
|
|
24
|
+
throw new UnavailabilityError('MediaLibrary', 'getIsInCloud is only available on iOS');
|
|
25
|
+
}
|
|
26
|
+
return super.getIsInCloud();
|
|
27
|
+
}
|
|
28
|
+
getOrientation() {
|
|
29
|
+
if (Platform.OS !== 'ios') {
|
|
30
|
+
throw new UnavailabilityError('MediaLibrary', 'getOrientation is only available on iOS');
|
|
31
|
+
}
|
|
32
|
+
return super.getOrientation();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=asset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset.js","sourceRoot":"","sources":["../../src/next/asset.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAGvD;;;;GAIG;AACH,MAAM,OAAO,KAAM,SAAQ,oBAAoB,CAAC,KAAK;IAC1C,gBAAgB;QACvB,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;YAC1B,MAAM,IAAI,mBAAmB,CAC3B,cAAc,EACd,2CAA2C,CAC5C,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC,gBAAgB,EAA6B,CAAC;IAC7D,CAAC;IAEQ,oBAAoB;QAC3B,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;YAC1B,MAAM,IAAI,mBAAmB,CAC3B,cAAc,EACd,+CAA+C,CAChD,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC,oBAAoB,EAAE,CAAC;IACtC,CAAC;IAEQ,YAAY;QACnB,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;YAC1B,MAAM,IAAI,mBAAmB,CAC3B,cAAc,EACd,uCAAuC,CACxC,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC,YAAY,EAAE,CAAC;IAC9B,CAAC;IAEQ,cAAc;QACrB,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;YAC1B,MAAM,IAAI,mBAAmB,CAC3B,cAAc,EACd,yCAAyC,CAC1C,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC,cAAc,EAAE,CAAC;IAChC,CAAC;CACF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type EventSubscription } from 'expo-modules-core';
|
|
2
|
+
import type { IGranularPermission, IMediaLibraryAssetsChangeEvent, IMediaLibraryNextPermissionResponse, IMediaTypeFilter, IPermissionHookOptions } from './types';
|
|
3
|
+
export { Query } from './query';
|
|
4
|
+
export { Asset } from './asset';
|
|
5
|
+
export { Album } from './album';
|
|
6
|
+
export * from './types';
|
|
7
|
+
/**
|
|
8
|
+
* @param granularPermissions Only has an effect on Android 13+. Defaults to every possible
|
|
9
|
+
* permission — make sure your app manifest requests everything you pass here.
|
|
10
|
+
*/
|
|
11
|
+
export declare function requestPermissionsAsync(writeOnly?: boolean, granularPermissions?: IGranularPermission[]): Promise<IMediaLibraryNextPermissionResponse>;
|
|
12
|
+
export declare function getPermissionsAsync(writeOnly?: boolean, granularPermissions?: IGranularPermission[]): Promise<IMediaLibraryNextPermissionResponse>;
|
|
13
|
+
export declare const usePermissions: (options?: import("expo-modules-core").PermissionHookOptions<IPermissionHookOptions> | undefined) => [IMediaLibraryNextPermissionResponse | null, () => Promise<IMediaLibraryNextPermissionResponse>, () => Promise<IMediaLibraryNextPermissionResponse>];
|
|
14
|
+
/**
|
|
15
|
+
* Allows the user to update the assets your app has access to. Only a no-op unless the user
|
|
16
|
+
* previously granted `'limited'` access.
|
|
17
|
+
*/
|
|
18
|
+
export declare function presentPermissionsPicker(mediaTypes?: IMediaTypeFilter[]): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Subscribes for updates in the user's media library. On Android it's invoked with an empty
|
|
21
|
+
* object — only iOS reports `insertedAssets`/`deletedAssets`/`updatedAssets`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function addListener(listener: (event: IMediaLibraryAssetsChangeEvent) => void): EventSubscription;
|
|
24
|
+
export declare function removeAllListeners(): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import { createPermissionHook, } from 'expo-modules-core';
|
|
3
|
+
import { expoMediaLibraryNext } from './native-module';
|
|
4
|
+
export { Query } from './query';
|
|
5
|
+
export { Asset } from './asset';
|
|
6
|
+
export { Album } from './album';
|
|
7
|
+
export * from './types';
|
|
8
|
+
/**
|
|
9
|
+
* @param granularPermissions Only has an effect on Android 13+. Defaults to every possible
|
|
10
|
+
* permission — make sure your app manifest requests everything you pass here.
|
|
11
|
+
*/
|
|
12
|
+
export async function requestPermissionsAsync(writeOnly = false, granularPermissions) {
|
|
13
|
+
if (Platform.OS === 'android') {
|
|
14
|
+
return expoMediaLibraryNext.requestPermissionsAsync(writeOnly, granularPermissions);
|
|
15
|
+
}
|
|
16
|
+
return expoMediaLibraryNext.requestPermissionsAsync(writeOnly);
|
|
17
|
+
}
|
|
18
|
+
export async function getPermissionsAsync(writeOnly = false, granularPermissions) {
|
|
19
|
+
if (Platform.OS === 'android') {
|
|
20
|
+
return expoMediaLibraryNext.getPermissionsAsync(writeOnly, granularPermissions);
|
|
21
|
+
}
|
|
22
|
+
return expoMediaLibraryNext.getPermissionsAsync(writeOnly);
|
|
23
|
+
}
|
|
24
|
+
export const usePermissions = createPermissionHook({
|
|
25
|
+
getMethod: (options) => getPermissionsAsync(options?.writeOnly, options?.granularPermissions),
|
|
26
|
+
requestMethod: (options) => requestPermissionsAsync(options?.writeOnly, options?.granularPermissions),
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* Allows the user to update the assets your app has access to. Only a no-op unless the user
|
|
30
|
+
* previously granted `'limited'` access.
|
|
31
|
+
*/
|
|
32
|
+
export async function presentPermissionsPicker(mediaTypes) {
|
|
33
|
+
return expoMediaLibraryNext.presentPermissionsPicker(mediaTypes);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Subscribes for updates in the user's media library. On Android it's invoked with an empty
|
|
37
|
+
* object — only iOS reports `insertedAssets`/`deletedAssets`/`updatedAssets`.
|
|
38
|
+
*/
|
|
39
|
+
export function addListener(listener) {
|
|
40
|
+
return expoMediaLibraryNext.addListener('mediaLibraryDidChange', listener);
|
|
41
|
+
}
|
|
42
|
+
export function removeAllListeners() {
|
|
43
|
+
expoMediaLibraryNext.removeAllListeners('mediaLibraryDidChange');
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EACL,oBAAoB,GAErB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AASvD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,cAAc,SAAS,CAAC;AAExB;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,YAAqB,KAAK,EAC1B,mBAA2C;IAE3C,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,oBAAoB,CAAC,uBAAuB,CACjD,SAAS,EACT,mBAAmB,CACpB,CAAC;IACJ,CAAC;IACD,OAAO,oBAAoB,CAAC,uBAAuB,CAAC,SAAS,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,YAAqB,KAAK,EAC1B,mBAA2C;IAE3C,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,oBAAoB,CAAC,mBAAmB,CAC7C,SAAS,EACT,mBAAmB,CACpB,CAAC;IACJ,CAAC;IACD,OAAO,oBAAoB,CAAC,mBAAmB,CAAC,SAAS,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,oBAAoB,CAGhD;IACA,SAAS,EAAE,CAAC,OAAgC,EAAE,EAAE,CAC9C,mBAAmB,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,mBAAmB,CAAC;IACvE,aAAa,EAAE,CAAC,OAAgC,EAAE,EAAE,CAClD,uBAAuB,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,mBAAmB,CAAC;CAC5E,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,UAA+B;IAE/B,OAAO,oBAAoB,CAAC,wBAAwB,CAAC,UAAU,CAAC,CAAC;AACnE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,QAAyD;IAEzD,OAAO,oBAAoB,CAAC,WAAW,CAAC,uBAAuB,EAAE,QAAQ,CAAC,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,oBAAoB,CAAC,kBAAkB,CAAC,uBAAuB,CAAC,CAAC;AACnE,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type EventSubscription } from 'expo-modules-core';
|
|
2
|
+
import type { AssetField, IAssetFieldValueMap, IAssetInfo, IAssetMetadata, ISortDescriptor } from './types';
|
|
3
|
+
export declare class NativeMediaLibraryQuery {
|
|
4
|
+
constructor();
|
|
5
|
+
eq<T extends AssetField>(field: T, value: IAssetFieldValueMap[T]): this;
|
|
6
|
+
within<T extends AssetField>(field: T, value: IAssetFieldValueMap[T][]): this;
|
|
7
|
+
gt(field: AssetField, value: number): this;
|
|
8
|
+
gte(field: AssetField, value: number): this;
|
|
9
|
+
lt(field: AssetField, value: number): this;
|
|
10
|
+
lte(field: AssetField, value: number): this;
|
|
11
|
+
limit(limit: number): this;
|
|
12
|
+
offset(offset: number): this;
|
|
13
|
+
orderBy(sortDescriptors: ISortDescriptor | AssetField): this;
|
|
14
|
+
album(album: NativeMediaLibraryAlbum): this;
|
|
15
|
+
exe(): Promise<NativeMediaLibraryAsset[]>;
|
|
16
|
+
exeForMetadata(): Promise<IAssetMetadata[]>;
|
|
17
|
+
}
|
|
18
|
+
export declare class NativeMediaLibraryAsset {
|
|
19
|
+
constructor(id: string);
|
|
20
|
+
id: string;
|
|
21
|
+
getCreationTime(): Promise<number | null>;
|
|
22
|
+
getDuration(): Promise<number | null>;
|
|
23
|
+
getFilename(): Promise<string>;
|
|
24
|
+
getHeight(): Promise<number>;
|
|
25
|
+
getMediaType(): Promise<string>;
|
|
26
|
+
getMediaSubtypes(): Promise<string[]>;
|
|
27
|
+
getLivePhotoVideoUri(): Promise<string | null>;
|
|
28
|
+
getIsInCloud(): Promise<boolean>;
|
|
29
|
+
getOrientation(): Promise<number | null>;
|
|
30
|
+
getModificationTime(): Promise<number | null>;
|
|
31
|
+
getShape(): Promise<{
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
} | null>;
|
|
35
|
+
getUri(): Promise<string>;
|
|
36
|
+
getWidth(): Promise<number>;
|
|
37
|
+
getInfo(): Promise<IAssetInfo>;
|
|
38
|
+
getAlbums(): Promise<NativeMediaLibraryAlbum[]>;
|
|
39
|
+
getLocation(): Promise<{
|
|
40
|
+
latitude: number;
|
|
41
|
+
longitude: number;
|
|
42
|
+
} | null>;
|
|
43
|
+
getExif(): Promise<Record<string, unknown>>;
|
|
44
|
+
delete(): Promise<void>;
|
|
45
|
+
getFavorite(): Promise<boolean>;
|
|
46
|
+
setFavorite(isFavorite: boolean): Promise<void>;
|
|
47
|
+
static create(filePath: string, album?: NativeMediaLibraryAlbum): Promise<NativeMediaLibraryAsset>;
|
|
48
|
+
static delete(assets: NativeMediaLibraryAsset[]): Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
export declare class NativeMediaLibraryAlbum {
|
|
51
|
+
constructor(id: string);
|
|
52
|
+
id: string;
|
|
53
|
+
getAssets(): Promise<NativeMediaLibraryAsset[]>;
|
|
54
|
+
getTitle(): Promise<string>;
|
|
55
|
+
delete(): Promise<void>;
|
|
56
|
+
add(assets: NativeMediaLibraryAsset | NativeMediaLibraryAsset[]): Promise<void>;
|
|
57
|
+
removeAssets(assets: NativeMediaLibraryAsset[]): Promise<void>;
|
|
58
|
+
static create(name: string, assetsRefs: string[] | NativeMediaLibraryAsset[], moveAssets?: boolean): Promise<NativeMediaLibraryAlbum>;
|
|
59
|
+
static delete(albums: NativeMediaLibraryAlbum[], deleteAssets?: boolean): Promise<void>;
|
|
60
|
+
static get(title: string): Promise<NativeMediaLibraryAlbum | null>;
|
|
61
|
+
static getAll(): Promise<NativeMediaLibraryAlbum[]>;
|
|
62
|
+
}
|
|
63
|
+
export type INativeMediaLibraryNextModule = {
|
|
64
|
+
Query: typeof NativeMediaLibraryQuery;
|
|
65
|
+
Asset: typeof NativeMediaLibraryAsset;
|
|
66
|
+
Album: typeof NativeMediaLibraryAlbum;
|
|
67
|
+
getPermissionsAsync(writeOnly?: boolean, granularPermissions?: string[]): Promise<import('expo-modules-core').PermissionResponse>;
|
|
68
|
+
requestPermissionsAsync(writeOnly?: boolean, granularPermissions?: string[]): Promise<import('expo-modules-core').PermissionResponse>;
|
|
69
|
+
presentPermissionsPicker(mediaTypes?: string[]): Promise<void>;
|
|
70
|
+
addListener(eventName: 'mediaLibraryDidChange', listener: (event: {
|
|
71
|
+
hasIncrementalChanges: boolean;
|
|
72
|
+
insertedAssets?: string[];
|
|
73
|
+
deletedAssets?: string[];
|
|
74
|
+
updatedAssets?: string[];
|
|
75
|
+
}) => void): EventSubscription;
|
|
76
|
+
removeAllListeners(eventName: 'mediaLibraryDidChange'): void;
|
|
77
|
+
};
|
|
78
|
+
export declare const expoMediaLibraryNext: INativeMediaLibraryNextModule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/next/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAA0B,MAAM,mBAAmB,CAAC;AAkHhF,MAAM,CAAC,MAAM,oBAAoB,GAC/B,mBAAmB,CAAgC,sBAAsB,CAAC,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expoMediaLibraryNext } from './native-module';
|
|
2
|
+
import { Asset } from './asset';
|
|
3
|
+
/**
|
|
4
|
+
* A query builder over the media library, matching upstream's own trivial subclass
|
|
5
|
+
* (`export class Query extends ExpoMediaLibraryNext.Query {}`) for every method but `exe()`:
|
|
6
|
+
* the native call constructs its results off the BASE native asset class, never our own `Asset`
|
|
7
|
+
* (asset.ts) — so without this override, a query result's iOS-only getters silently skip the
|
|
8
|
+
* platform guard every other `Asset` gets. Re-wrapping by id is cheap (a shared-object handle,
|
|
9
|
+
* not a data copy) and keeps `exe()` consistent with every other way of obtaining an `Asset`.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Query extends expoMediaLibraryNext.Query {
|
|
12
|
+
exe(): Promise<Asset[]>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { expoMediaLibraryNext } from './native-module';
|
|
2
|
+
import { Asset } from './asset';
|
|
3
|
+
/**
|
|
4
|
+
* A query builder over the media library, matching upstream's own trivial subclass
|
|
5
|
+
* (`export class Query extends ExpoMediaLibraryNext.Query {}`) for every method but `exe()`:
|
|
6
|
+
* the native call constructs its results off the BASE native asset class, never our own `Asset`
|
|
7
|
+
* (asset.ts) — so without this override, a query result's iOS-only getters silently skip the
|
|
8
|
+
* platform guard every other `Asset` gets. Re-wrapping by id is cheap (a shared-object handle,
|
|
9
|
+
* not a data copy) and keeps `exe()` consistent with every other way of obtaining an `Asset`.
|
|
10
|
+
*/
|
|
11
|
+
export class Query extends expoMediaLibraryNext.Query {
|
|
12
|
+
async exe() {
|
|
13
|
+
const results = await super.exe();
|
|
14
|
+
return results.map(result => new Asset(result.id));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/next/query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC;;;;;;;GAOG;AACH,MAAM,OAAO,KAAM,SAAQ,oBAAoB,CAAC,KAAK;IAC1C,KAAK,CAAC,GAAG;QAChB,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,CAAC;QAClC,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACrD,CAAC;CACF"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { PermissionResponse } from 'expo-modules-core';
|
|
2
|
+
export declare enum AssetField {
|
|
3
|
+
CREATION_TIME = "creationTime",
|
|
4
|
+
MODIFICATION_TIME = "modificationTime",
|
|
5
|
+
MEDIA_TYPE = "mediaType",
|
|
6
|
+
WIDTH = "width",
|
|
7
|
+
HEIGHT = "height",
|
|
8
|
+
DURATION = "duration",
|
|
9
|
+
IS_FAVORITE = "isFavorite"
|
|
10
|
+
}
|
|
11
|
+
export declare enum MediaType {
|
|
12
|
+
UNKNOWN = "unknown",
|
|
13
|
+
IMAGE = "image",
|
|
14
|
+
AUDIO = "audio",
|
|
15
|
+
VIDEO = "video"
|
|
16
|
+
}
|
|
17
|
+
export declare enum MediaSubtype {
|
|
18
|
+
DEPTH_EFFECT = "depthEffect",
|
|
19
|
+
HDR = "hdr",
|
|
20
|
+
HIGH_FRAME_RATE = "highFrameRate",
|
|
21
|
+
LIVE_PHOTO = "livePhoto",
|
|
22
|
+
PANORAMA = "panorama",
|
|
23
|
+
SCREENSHOT = "screenshot",
|
|
24
|
+
STREAM = "stream",
|
|
25
|
+
TIME_LAPSE = "timelapse",
|
|
26
|
+
SPATIAL_MEDIA = "spatialMedia",
|
|
27
|
+
VIDEO_CINEMATIC = "videoCinematic"
|
|
28
|
+
}
|
|
29
|
+
export type IAssetFieldValueMap = {
|
|
30
|
+
[AssetField.CREATION_TIME]: number;
|
|
31
|
+
[AssetField.MODIFICATION_TIME]: number;
|
|
32
|
+
[AssetField.MEDIA_TYPE]: MediaType;
|
|
33
|
+
[AssetField.WIDTH]: number;
|
|
34
|
+
[AssetField.HEIGHT]: number;
|
|
35
|
+
[AssetField.DURATION]: number;
|
|
36
|
+
[AssetField.IS_FAVORITE]: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type ISortDescriptor = {
|
|
39
|
+
key: AssetField;
|
|
40
|
+
ascending?: boolean;
|
|
41
|
+
};
|
|
42
|
+
export type IShape = {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
};
|
|
46
|
+
export type ILocation = {
|
|
47
|
+
latitude: number;
|
|
48
|
+
longitude: number;
|
|
49
|
+
};
|
|
50
|
+
export type IAssetInfo = {
|
|
51
|
+
id: string;
|
|
52
|
+
filename: string;
|
|
53
|
+
uri: string;
|
|
54
|
+
mediaType: MediaType;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
duration: number | null;
|
|
58
|
+
creationTime: number | null;
|
|
59
|
+
modificationTime: number | null;
|
|
60
|
+
isFavorite: boolean;
|
|
61
|
+
};
|
|
62
|
+
export type IAssetMetadata = {
|
|
63
|
+
id: string;
|
|
64
|
+
filename: string | null;
|
|
65
|
+
mediaType: MediaType;
|
|
66
|
+
width: number | null;
|
|
67
|
+
height: number | null;
|
|
68
|
+
duration: number | null;
|
|
69
|
+
creationTime: number | null;
|
|
70
|
+
modificationTime: number | null;
|
|
71
|
+
isFavorite: boolean;
|
|
72
|
+
};
|
|
73
|
+
export type IGranularPermission = 'audio' | 'photo' | 'video';
|
|
74
|
+
export type IMediaTypeFilter = 'photo' | 'video';
|
|
75
|
+
export type IMediaLibraryAssetsChangeEvent = {
|
|
76
|
+
hasIncrementalChanges: boolean;
|
|
77
|
+
insertedAssets?: string[];
|
|
78
|
+
deletedAssets?: string[];
|
|
79
|
+
updatedAssets?: string[];
|
|
80
|
+
};
|
|
81
|
+
export type IPermissionHookOptions = {
|
|
82
|
+
writeOnly?: boolean;
|
|
83
|
+
granularPermissions?: IGranularPermission[];
|
|
84
|
+
};
|
|
85
|
+
export type IMediaLibraryNextPermissionResponse = PermissionResponse & {
|
|
86
|
+
/**
|
|
87
|
+
* - `'all'` — full library access
|
|
88
|
+
* - `'limited'` — only selected photos (Android 14+, iOS 14+)
|
|
89
|
+
* - `'none'` — denied or not yet granted
|
|
90
|
+
*/
|
|
91
|
+
accessPrivileges?: 'all' | 'limited' | 'none';
|
|
92
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export var AssetField;
|
|
2
|
+
(function (AssetField) {
|
|
3
|
+
AssetField["CREATION_TIME"] = "creationTime";
|
|
4
|
+
AssetField["MODIFICATION_TIME"] = "modificationTime";
|
|
5
|
+
AssetField["MEDIA_TYPE"] = "mediaType";
|
|
6
|
+
AssetField["WIDTH"] = "width";
|
|
7
|
+
AssetField["HEIGHT"] = "height";
|
|
8
|
+
AssetField["DURATION"] = "duration";
|
|
9
|
+
AssetField["IS_FAVORITE"] = "isFavorite";
|
|
10
|
+
})(AssetField || (AssetField = {}));
|
|
11
|
+
export var MediaType;
|
|
12
|
+
(function (MediaType) {
|
|
13
|
+
MediaType["UNKNOWN"] = "unknown";
|
|
14
|
+
MediaType["IMAGE"] = "image";
|
|
15
|
+
MediaType["AUDIO"] = "audio";
|
|
16
|
+
MediaType["VIDEO"] = "video";
|
|
17
|
+
})(MediaType || (MediaType = {}));
|
|
18
|
+
export var MediaSubtype;
|
|
19
|
+
(function (MediaSubtype) {
|
|
20
|
+
MediaSubtype["DEPTH_EFFECT"] = "depthEffect";
|
|
21
|
+
MediaSubtype["HDR"] = "hdr";
|
|
22
|
+
MediaSubtype["HIGH_FRAME_RATE"] = "highFrameRate";
|
|
23
|
+
MediaSubtype["LIVE_PHOTO"] = "livePhoto";
|
|
24
|
+
MediaSubtype["PANORAMA"] = "panorama";
|
|
25
|
+
MediaSubtype["SCREENSHOT"] = "screenshot";
|
|
26
|
+
MediaSubtype["STREAM"] = "stream";
|
|
27
|
+
MediaSubtype["TIME_LAPSE"] = "timelapse";
|
|
28
|
+
MediaSubtype["SPATIAL_MEDIA"] = "spatialMedia";
|
|
29
|
+
MediaSubtype["VIDEO_CINEMATIC"] = "videoCinematic";
|
|
30
|
+
})(MediaSubtype || (MediaSubtype = {}));
|
|
31
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/next/types.ts"],"names":[],"mappings":"AAEA,MAAM,CAAN,IAAY,UAQX;AARD,WAAY,UAAU;IACpB,4CAA8B,CAAA;IAC9B,oDAAsC,CAAA;IACtC,sCAAwB,CAAA;IACxB,6BAAe,CAAA;IACf,+BAAiB,CAAA;IACjB,mCAAqB,CAAA;IACrB,wCAA0B,CAAA;AAC5B,CAAC,EARW,UAAU,KAAV,UAAU,QAQrB;AAED,MAAM,CAAN,IAAY,SAKX;AALD,WAAY,SAAS;IACnB,gCAAmB,CAAA;IACnB,4BAAe,CAAA;IACf,4BAAe,CAAA;IACf,4BAAe,CAAA;AACjB,CAAC,EALW,SAAS,KAAT,SAAS,QAKpB;AAED,MAAM,CAAN,IAAY,YAWX;AAXD,WAAY,YAAY;IACtB,4CAA4B,CAAA;IAC5B,2BAAW,CAAA;IACX,iDAAiC,CAAA;IACjC,wCAAwB,CAAA;IACxB,qCAAqB,CAAA;IACrB,yCAAyB,CAAA;IACzB,iCAAiB,CAAA;IACjB,wCAAwB,CAAA;IACxB,8CAA8B,CAAA;IAC9B,kDAAkC,CAAA;AACpC,CAAC,EAXW,YAAY,KAAZ,YAAY,QAWvB"}
|
package/native-link.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"android": {
|
|
3
|
+
"gradleProjectName": "expo-media-library",
|
|
4
|
+
"modules": [
|
|
5
|
+
{
|
|
6
|
+
"importPath": "expo.modules.medialibrary.MediaLibraryModule",
|
|
7
|
+
"className": "MediaLibraryModule",
|
|
8
|
+
"nativeName": "ExpoMediaLibrary"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"importPath": "expo.modules.medialibrary.next.MediaLibraryNextModule",
|
|
12
|
+
"className": "MediaLibraryNextModule",
|
|
13
|
+
"nativeName": "ExpoMediaLibraryNext"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"manifestPermissions": [
|
|
17
|
+
"android.permission.READ_EXTERNAL_STORAGE",
|
|
18
|
+
"android.permission.WRITE_EXTERNAL_STORAGE",
|
|
19
|
+
"android.permission.READ_MEDIA_VISUAL_USER_SELECTED",
|
|
20
|
+
"android.permission.READ_MEDIA_IMAGES",
|
|
21
|
+
"android.permission.READ_MEDIA_VIDEO",
|
|
22
|
+
"android.permission.READ_MEDIA_AUDIO"
|
|
23
|
+
],
|
|
24
|
+
"manifestApplicationAttributes": {
|
|
25
|
+
"android:requestLegacyExternalStorage": "true"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"ios": {
|
|
29
|
+
"infoPlistKeys": {
|
|
30
|
+
"NSPhotoLibraryUsageDescription": "Allow $(PRODUCT_NAME) to access your photos.",
|
|
31
|
+
"NSPhotoLibraryAddUsageDescription": "Allow $(PRODUCT_NAME) to save photos."
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|