@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.
- package/ExpoPhotos.podspec +1 -1
- package/README.md +1 -0
- package/build/PHImageSource.d.ts +32 -0
- package/build/PHImageSource.d.ts.map +1 -0
- package/build/PHImageSource.js +51 -0
- package/build/PHImageSource.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/expo-module.config.json +1 -2
- package/ios/ExpoPhotos.podspec +24 -0
- package/ios/ExpoPhotosImageLoader.swift +183 -0
- package/package.json +11 -1
- package/src/PHImageSource.tsx +91 -0
- package/src/index.ts +2 -0
package/ExpoPhotos.podspec
CHANGED
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
package/build/index.d.ts.map
CHANGED
|
@@ -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
package/build/index.js.map
CHANGED
|
@@ -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"]}
|
package/expo-module.config.json
CHANGED
|
@@ -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": "
|
|
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