@hortemo/expo-photos 1.0.0 → 2.0.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.
@@ -16,7 +16,7 @@ Pod::Spec.new do |s|
16
16
  s.static_framework = true
17
17
 
18
18
  s.dependency 'ExpoModulesCore'
19
- s.dependency 'React'
19
+ s.dependency 'React-Core'
20
20
  s.dependency 'SDWebImageWebPCoder', '0.15.0'
21
21
 
22
22
  s.source_files = 'ios/**/*.{h,m,swift}'
package/README.md CHANGED
@@ -16,3 +16,4 @@ npm install @hortemo/expo-photos
16
16
  - `pickAssets(options: PickAssetsOptions): Promise<PHAsset[]>`
17
17
  - `requestAuthorization(accessLevel: PHAccessLevel): Promise<PHAuthorizationStatus>`
18
18
  - `presentLimitedLibraryPicker(): Promise<void>`
19
+ - `createPHImageSource(options: PHImageSourceOptions): ImageSourcePropType | undefined`
@@ -0,0 +1,32 @@
1
+ import type { ImageSourcePropType } from "react-native";
2
+ import { CGSize, PHImageContentMode, PHImageRequestOptionsDeliveryMode, PHImageRequestOptionsResizeMode } from "./ExpoPhotos.types";
3
+ export interface PHImageSourceOptions {
4
+ localIdentifier: string;
5
+ targetSize?: CGSize;
6
+ resizeMode?: PHImageRequestOptionsResizeMode;
7
+ deliveryMode?: PHImageRequestOptionsDeliveryMode;
8
+ contentMode?: PHImageContentMode;
9
+ isNetworkAccessAllowed?: boolean;
10
+ }
11
+ /**
12
+ * Creates a source object for React Native's Image component from a Photos library asset identifier.
13
+ *
14
+ * @param options - Configuration for image source, including the local identifier
15
+ * @returns A source object that can be used with the Image component's `source` prop, or undefined if no identifier is provided
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * import { Image } from 'react-native';
20
+ * import { createPHImageSource } from '@hortemo/expo-photos';
21
+ *
22
+ * <Image
23
+ * source={createPHImageSource({
24
+ * localIdentifier: asset.localIdentifier,
25
+ * targetSize: { width: 200, height: 200 },
26
+ * contentMode: PHImageContentMode.aspectFill,
27
+ * })}
28
+ * />
29
+ * ```
30
+ */
31
+ export declare function createPHImageSource(options: PHImageSourceOptions): ImageSourcePropType | undefined;
32
+ //# sourceMappingURL=PHImageSource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PHImageSource.d.ts","sourceRoot":"","sources":["../src/PHImageSource.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EACL,MAAM,EACN,kBAAkB,EAClB,iCAAiC,EACjC,+BAA+B,EAChC,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,oBAAoB;IACnC,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,+BAA+B,CAAC;IAC7C,YAAY,CAAC,EAAE,iCAAiC,CAAC;IACjD,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAwBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,GAC5B,mBAAmB,GAAG,SAAS,CA6BjC"}
@@ -0,0 +1,51 @@
1
+ import { PHImageContentMode, PHImageRequestOptionsDeliveryMode, PHImageRequestOptionsResizeMode, } from "./ExpoPhotos.types";
2
+ const buildQueryString = (params) => {
3
+ const entries = Object.entries(params).filter(([, value]) => value !== undefined);
4
+ if (entries.length === 0) {
5
+ return "";
6
+ }
7
+ return ("?" +
8
+ entries
9
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value ?? "")}`)
10
+ .join("&"));
11
+ };
12
+ /**
13
+ * Creates a source object for React Native's Image component from a Photos library asset identifier.
14
+ *
15
+ * @param options - Configuration for image source, including the local identifier
16
+ * @returns A source object that can be used with the Image component's `source` prop, or undefined if no identifier is provided
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import { Image } from 'react-native';
21
+ * import { createPHImageSource } from '@hortemo/expo-photos';
22
+ *
23
+ * <Image
24
+ * source={createPHImageSource({
25
+ * localIdentifier: asset.localIdentifier,
26
+ * targetSize: { width: 200, height: 200 },
27
+ * contentMode: PHImageContentMode.aspectFill,
28
+ * })}
29
+ * />
30
+ * ```
31
+ */
32
+ export function createPHImageSource(options) {
33
+ const { localIdentifier } = options;
34
+ if (!localIdentifier) {
35
+ return undefined;
36
+ }
37
+ const { targetSize, resizeMode = PHImageRequestOptionsResizeMode.Fast, deliveryMode = PHImageRequestOptionsDeliveryMode.Opportunistic, contentMode = PHImageContentMode.aspectFill, isNetworkAccessAllowed = true, } = options;
38
+ const targetSizeValue = targetSize
39
+ ? `${targetSize.width}x${targetSize.height}`
40
+ : undefined;
41
+ const query = buildQueryString({
42
+ localIdentifier,
43
+ targetSize: targetSizeValue,
44
+ resizeMode: resizeMode.toString(),
45
+ deliveryMode: deliveryMode.toString(),
46
+ contentMode: contentMode.toString(),
47
+ isNetworkAccessAllowed: isNetworkAccessAllowed.toString(),
48
+ });
49
+ return { uri: `expo-photos://${query}` };
50
+ }
51
+ //# sourceMappingURL=PHImageSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PHImageSource.js","sourceRoot":"","sources":["../src/PHImageSource.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,kBAAkB,EAClB,iCAAiC,EACjC,+BAA+B,GAChC,MAAM,oBAAoB,CAAC;AAW5B,MAAM,gBAAgB,GAAG,CACvB,MAA0C,EAClC,EAAE;IACV,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAC3C,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CACnC,CAAC;IAEF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,CACL,GAAG;QACH,OAAO;aACJ,GAAG,CACF,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CACf,GAAG,kBAAkB,CAAC,GAAG,CAAC,IAAI,kBAAkB,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAClE;aACA,IAAI,CAAC,GAAG,CAAC,CACb,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAA6B;IAE7B,MAAM,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC;IAEpC,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,EACJ,UAAU,EACV,UAAU,GAAG,+BAA+B,CAAC,IAAI,EACjD,YAAY,GAAG,iCAAiC,CAAC,aAAa,EAC9D,WAAW,GAAG,kBAAkB,CAAC,UAAU,EAC3C,sBAAsB,GAAG,IAAI,GAC9B,GAAG,OAAO,CAAC;IAEZ,MAAM,eAAe,GAAG,UAAU;QAChC,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,IAAI,UAAU,CAAC,MAAM,EAAE;QAC5C,CAAC,CAAC,SAAS,CAAC;IAEd,MAAM,KAAK,GAAG,gBAAgB,CAAC;QAC7B,eAAe;QACf,UAAU,EAAE,eAAe;QAC3B,UAAU,EAAE,UAAU,CAAC,QAAQ,EAAE;QACjC,YAAY,EAAE,YAAY,CAAC,QAAQ,EAAE;QACrC,WAAW,EAAE,WAAW,CAAC,QAAQ,EAAE;QACnC,sBAAsB,EAAE,sBAAsB,CAAC,QAAQ,EAAE;KAC1D,CAAC,CAAC;IAEH,OAAO,EAAE,GAAG,EAAE,iBAAiB,KAAK,EAAE,EAAE,CAAC;AAC3C,CAAC","sourcesContent":["import type { ImageSourcePropType } from \"react-native\";\nimport {\n CGSize,\n PHImageContentMode,\n PHImageRequestOptionsDeliveryMode,\n PHImageRequestOptionsResizeMode,\n} from \"./ExpoPhotos.types\";\n\nexport interface PHImageSourceOptions {\n localIdentifier: string;\n targetSize?: CGSize;\n resizeMode?: PHImageRequestOptionsResizeMode;\n deliveryMode?: PHImageRequestOptionsDeliveryMode;\n contentMode?: PHImageContentMode;\n isNetworkAccessAllowed?: boolean;\n}\n\nconst buildQueryString = (\n params: Record<string, string | undefined>\n): string => {\n const entries = Object.entries(params).filter(\n ([, value]) => value !== undefined\n );\n\n if (entries.length === 0) {\n return \"\";\n }\n\n return (\n \"?\" +\n entries\n .map(\n ([key, value]) =>\n `${encodeURIComponent(key)}=${encodeURIComponent(value ?? \"\")}`\n )\n .join(\"&\")\n );\n};\n\n/**\n * Creates a source object for React Native's Image component from a Photos library asset identifier.\n *\n * @param options - Configuration for image source, including the local identifier\n * @returns A source object that can be used with the Image component's `source` prop, or undefined if no identifier is provided\n *\n * @example\n * ```tsx\n * import { Image } from 'react-native';\n * import { createPHImageSource } from '@hortemo/expo-photos';\n *\n * <Image\n * source={createPHImageSource({\n * localIdentifier: asset.localIdentifier,\n * targetSize: { width: 200, height: 200 },\n * contentMode: PHImageContentMode.aspectFill,\n * })}\n * />\n * ```\n */\nexport function createPHImageSource(\n options: PHImageSourceOptions\n): ImageSourcePropType | undefined {\n const { localIdentifier } = options;\n\n if (!localIdentifier) {\n return undefined;\n }\n\n const {\n targetSize,\n resizeMode = PHImageRequestOptionsResizeMode.Fast,\n deliveryMode = PHImageRequestOptionsDeliveryMode.Opportunistic,\n contentMode = PHImageContentMode.aspectFill,\n isNetworkAccessAllowed = true,\n } = options;\n\n const targetSizeValue = targetSize\n ? `${targetSize.width}x${targetSize.height}`\n : undefined;\n\n const query = buildQueryString({\n localIdentifier,\n targetSize: targetSizeValue,\n resizeMode: resizeMode.toString(),\n deliveryMode: deliveryMode.toString(),\n contentMode: contentMode.toString(),\n isNetworkAccessAllowed: isNetworkAccessAllowed.toString(),\n });\n\n return { uri: `expo-photos://${query}` };\n}\n"]}
package/build/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export { default } from "./ExpoPhotosModule";
2
2
  export * from "./ExpoPhotos.types";
3
+ export { createPHImageSource } from "./PHImageSource";
4
+ export type { PHImageSourceOptions } from "./PHImageSource";
3
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC"}
package/build/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { default } from "./ExpoPhotosModule";
2
2
  export * from "./ExpoPhotos.types";
3
+ export { createPHImageSource } from "./PHImageSource";
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,cAAc,oBAAoB,CAAC","sourcesContent":["export { default } from \"./ExpoPhotosModule\";\nexport * from \"./ExpoPhotos.types\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["export { default } from \"./ExpoPhotosModule\";\nexport * from \"./ExpoPhotos.types\";\nexport { createPHImageSource } from \"./PHImageSource\";\nexport type { PHImageSourceOptions } from \"./PHImageSource\";\n"]}
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "platforms": ["apple"],
3
3
  "apple": {
4
- "modules": ["ExpoPhotos"],
5
- "podspecPath": "ExpoPhotos.podspec"
4
+ "modules": ["ExpoPhotos"]
6
5
  }
7
6
  }
@@ -0,0 +1,24 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'ExpoPhotos'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.description = package['description']
10
+ s.license = package['license']
11
+ s.author = package['author']
12
+ s.homepage = package['homepage']
13
+ s.platform = :ios, '15.0'
14
+ s.swift_version = '5.9'
15
+ s.source = { git: package['repository'] || 'https://github.com/hortemo/expo-photos' }
16
+ s.static_framework = true
17
+
18
+ s.dependency 'ExpoModulesCore'
19
+ s.dependency 'React-Core'
20
+ s.dependency 'SDWebImageWebPCoder', '0.15.0'
21
+
22
+ s.source_files = '**/*.{h,m,swift}'
23
+ s.requires_arc = true
24
+ end
@@ -0,0 +1,183 @@
1
+ #if canImport(React)
2
+ import Foundation
3
+ import Photos
4
+ import React
5
+ import UIKit
6
+
7
+ @objc(ExpoPhotosImageLoader)
8
+ class ExpoPhotosImageLoader: NSObject, RCTBridgeModule, RCTImageURLLoader {
9
+ static func moduleName() -> String! {
10
+ return "ExpoPhotosImageLoader"
11
+ }
12
+
13
+ static func requiresMainQueueSetup() -> Bool {
14
+ return false
15
+ }
16
+
17
+ // MARK: - Helper Functions
18
+
19
+ private static func queryItems(from url: URL) -> [String: String] {
20
+ guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
21
+ let queryItems = components.queryItems else {
22
+ return [:]
23
+ }
24
+
25
+ var result: [String: String] = [:]
26
+ for item in queryItems {
27
+ if let value = item.value {
28
+ result[item.name] = value
29
+ }
30
+ }
31
+ return result
32
+ }
33
+
34
+ // MARK: - RCTImageURLLoader
35
+
36
+ func canLoadImageURL(_ requestURL: URL) -> Bool {
37
+ return requestURL.scheme?.caseInsensitiveCompare("expo-photos") == .orderedSame
38
+ }
39
+
40
+ func loaderPriority() -> Float {
41
+ return 1.0
42
+ }
43
+
44
+ func loadImage(
45
+ for imageURL: URL,
46
+ size: CGSize,
47
+ scale: CGFloat,
48
+ resizeMode: RCTResizeMode,
49
+ progressHandler: RCTImageLoaderProgressBlock!,
50
+ partialLoadHandler: RCTImageLoaderPartialLoadBlock!,
51
+ completionHandler: @escaping RCTImageLoaderCompletionBlock
52
+ ) -> RCTImageLoaderCancellationBlock! {
53
+
54
+ guard imageURL.scheme?.caseInsensitiveCompare("expo-photos") == .orderedSame else {
55
+ completionHandler(RCTErrorWithMessage("Unsupported image URL scheme for ExpoPhotos"), nil)
56
+ return {}
57
+ }
58
+
59
+ let queryParams = Self.queryItems(from: imageURL)
60
+
61
+ guard let localIdentifier = queryParams["localIdentifier"], !localIdentifier.isEmpty else {
62
+ completionHandler(RCTErrorWithMessage("Missing localIdentifier query parameter"), nil)
63
+ return {}
64
+ }
65
+
66
+ let assets = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil)
67
+ guard let asset = assets.firstObject else {
68
+ completionHandler(RCTErrorWithMessage("Failed to fetch PHAsset with local identifier \(localIdentifier)"), nil)
69
+ return {}
70
+ }
71
+
72
+ let imageOptions = PHImageRequestOptions()
73
+
74
+ var targetSize = size.applying(CGAffineTransform(scaleX: scale, y: scale))
75
+
76
+ if let targetSizeString = queryParams["targetSize"] {
77
+ let components = targetSizeString.lowercased().components(separatedBy: "x")
78
+ guard components.count == 2,
79
+ let width = Double(components[0]),
80
+ let height = Double(components[1]) else {
81
+ completionHandler(RCTErrorWithMessage("Invalid targetSize format. Expected WIDTHxHEIGHT."), nil)
82
+ return {}
83
+ }
84
+ targetSize = CGSize(width: width, height: height)
85
+ }
86
+
87
+ if let deliveryModeString = queryParams["deliveryMode"] {
88
+ guard let deliveryMode = PHImageRequestOptionsDeliveryMode.from(queryParam: deliveryModeString) else {
89
+ completionHandler(RCTErrorWithMessage("Invalid deliveryMode value."), nil)
90
+ return {}
91
+ }
92
+ imageOptions.deliveryMode = deliveryMode
93
+ }
94
+
95
+ if let resizeModeString = queryParams["resizeMode"] {
96
+ guard let resizeMode = PHImageRequestOptionsResizeMode.from(queryParam: resizeModeString) else {
97
+ completionHandler(RCTErrorWithMessage("Invalid resizeMode value."), nil)
98
+ return {}
99
+ }
100
+ imageOptions.resizeMode = resizeMode
101
+ }
102
+
103
+ if let isNetworkAccessAllowedString = queryParams["isNetworkAccessAllowed"] {
104
+ imageOptions.isNetworkAccessAllowed = Bool.from(queryParam: isNetworkAccessAllowedString)
105
+ }
106
+
107
+ var contentMode: PHImageContentMode = .default
108
+ if let contentModeString = queryParams["contentMode"] {
109
+ guard let _contentMode = PHImageContentMode.from(queryParam: contentModeString) else {
110
+ completionHandler(RCTErrorWithMessage("Invalid contentMode value."), nil)
111
+ return {}
112
+ }
113
+ contentMode = _contentMode
114
+ }
115
+
116
+ if let progressHandler = progressHandler {
117
+ imageOptions.progressHandler = { progress, error, stop, info in
118
+ let multiplier: Int64 = 1_000_000
119
+ progressHandler(Int64(progress * Double(multiplier)), multiplier)
120
+ }
121
+ }
122
+
123
+ let requestID = PHImageManager.default().requestImage(
124
+ for: asset,
125
+ targetSize: targetSize,
126
+ contentMode: contentMode,
127
+ options: imageOptions
128
+ ) { result, info in
129
+ guard let info = info else {
130
+ return
131
+ }
132
+
133
+ let isCancelled = (info[PHImageCancelledKey] as? NSNumber)?.boolValue ?? false
134
+ let isDegraded = (info[PHImageResultIsDegradedKey] as? NSNumber)?.boolValue ?? false
135
+
136
+ if isCancelled {
137
+ return
138
+ }
139
+
140
+ if let result = result {
141
+ if isDegraded {
142
+ partialLoadHandler?(result)
143
+ } else {
144
+ completionHandler(nil, result)
145
+ }
146
+ } else if let error = info[PHImageErrorKey] as? Error {
147
+ completionHandler(error, nil)
148
+ }
149
+ }
150
+
151
+ return {
152
+ PHImageManager.default().cancelImageRequest(requestID)
153
+ }
154
+ }
155
+ }
156
+
157
+ // MARK: - RawRepresentable Extension
158
+
159
+ extension RawRepresentable where RawValue == Int {
160
+ static func from(queryParam string: String?) -> Self? {
161
+ guard let string = string, !string.isEmpty else {
162
+ return nil
163
+ }
164
+ guard let intValue = Int(string) else {
165
+ return nil
166
+ }
167
+ return Self(rawValue: intValue)
168
+ }
169
+ }
170
+
171
+ // MARK: - Bool Extension
172
+
173
+ extension Bool {
174
+ static func from(queryParam string: String?) -> Bool {
175
+ guard let string = string, !string.isEmpty else {
176
+ return false
177
+ }
178
+ return string.lowercased() == "true"
179
+ }
180
+ }
181
+
182
+ #endif
183
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hortemo/expo-photos",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Expo module exposing advanced iOS Photos APIs.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -39,5 +39,15 @@
39
39
  "expo": "*",
40
40
  "react": "*",
41
41
  "react-native": "*"
42
+ },
43
+ "codegenConfig": {
44
+ "name": "expo-photos",
45
+ "type": "modules",
46
+ "jsSrcsDir": "./src",
47
+ "ios": {
48
+ "modulesConformingToProtocol": {
49
+ "RCTImageURLLoader": "ExpoPhotosImageLoader"
50
+ }
51
+ }
42
52
  }
43
53
  }
@@ -0,0 +1,91 @@
1
+ import type { ImageSourcePropType } from "react-native";
2
+ import {
3
+ CGSize,
4
+ PHImageContentMode,
5
+ PHImageRequestOptionsDeliveryMode,
6
+ PHImageRequestOptionsResizeMode,
7
+ } from "./ExpoPhotos.types";
8
+
9
+ export interface PHImageSourceOptions {
10
+ localIdentifier: string;
11
+ targetSize?: CGSize;
12
+ resizeMode?: PHImageRequestOptionsResizeMode;
13
+ deliveryMode?: PHImageRequestOptionsDeliveryMode;
14
+ contentMode?: PHImageContentMode;
15
+ isNetworkAccessAllowed?: boolean;
16
+ }
17
+
18
+ const buildQueryString = (
19
+ params: Record<string, string | undefined>
20
+ ): string => {
21
+ const entries = Object.entries(params).filter(
22
+ ([, value]) => value !== undefined
23
+ );
24
+
25
+ if (entries.length === 0) {
26
+ return "";
27
+ }
28
+
29
+ return (
30
+ "?" +
31
+ entries
32
+ .map(
33
+ ([key, value]) =>
34
+ `${encodeURIComponent(key)}=${encodeURIComponent(value ?? "")}`
35
+ )
36
+ .join("&")
37
+ );
38
+ };
39
+
40
+ /**
41
+ * Creates a source object for React Native's Image component from a Photos library asset identifier.
42
+ *
43
+ * @param options - Configuration for image source, including the local identifier
44
+ * @returns A source object that can be used with the Image component's `source` prop, or undefined if no identifier is provided
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * import { Image } from 'react-native';
49
+ * import { createPHImageSource } from '@hortemo/expo-photos';
50
+ *
51
+ * <Image
52
+ * source={createPHImageSource({
53
+ * localIdentifier: asset.localIdentifier,
54
+ * targetSize: { width: 200, height: 200 },
55
+ * contentMode: PHImageContentMode.aspectFill,
56
+ * })}
57
+ * />
58
+ * ```
59
+ */
60
+ export function createPHImageSource(
61
+ options: PHImageSourceOptions
62
+ ): ImageSourcePropType | undefined {
63
+ const { localIdentifier } = options;
64
+
65
+ if (!localIdentifier) {
66
+ return undefined;
67
+ }
68
+
69
+ const {
70
+ targetSize,
71
+ resizeMode = PHImageRequestOptionsResizeMode.Fast,
72
+ deliveryMode = PHImageRequestOptionsDeliveryMode.Opportunistic,
73
+ contentMode = PHImageContentMode.aspectFill,
74
+ isNetworkAccessAllowed = true,
75
+ } = options;
76
+
77
+ const targetSizeValue = targetSize
78
+ ? `${targetSize.width}x${targetSize.height}`
79
+ : undefined;
80
+
81
+ const query = buildQueryString({
82
+ localIdentifier,
83
+ targetSize: targetSizeValue,
84
+ resizeMode: resizeMode.toString(),
85
+ deliveryMode: deliveryMode.toString(),
86
+ contentMode: contentMode.toString(),
87
+ isNetworkAccessAllowed: isNetworkAccessAllowed.toString(),
88
+ });
89
+
90
+ return { uri: `expo-photos://${query}` };
91
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export { default } from "./ExpoPhotosModule";
2
2
  export * from "./ExpoPhotos.types";
3
+ export { createPHImageSource } from "./PHImageSource";
4
+ export type { PHImageSourceOptions } from "./PHImageSource";