@hortemo/expo-photos 6.0.0 → 8.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/README.md CHANGED
@@ -19,10 +19,25 @@ npm install @hortemo/expo-photos
19
19
  - `authorizationStatus(accessLevel: PHAccessLevel): Promise<PHAuthorizationStatus>`
20
20
  - `requestAuthorization(accessLevel: PHAccessLevel): Promise<PHAuthorizationStatus>`
21
21
  - `presentLimitedLibraryPicker(): Promise<void>`
22
- - `createPHImageSource(options: PHImageSourceOptions): ImageSourcePropType | undefined`
23
22
 
24
23
  ### Components
25
24
 
25
+ #### PHImage
26
+
27
+ A native image component that displays photos from the Photos library using `PHImageManager`.
28
+
29
+ ```tsx
30
+ import { PHImage, PHImageContentMode } from "@hortemo/expo-photos";
31
+
32
+ <PHImage
33
+ localIdentifier={asset.localIdentifier}
34
+ contentMode={PHImageContentMode.aspectFill}
35
+ resizeMode={PHImageRequestOptionsResizeMode.Fast}
36
+ deliveryMode={PHImageRequestOptionsDeliveryMode.HighQualityFormat}
37
+ style={{ width: 300, height: 300 }}
38
+ />
39
+ ```
40
+
26
41
  #### PHVideo
27
42
 
28
43
  A video player component that streams videos from the library.
@@ -0,0 +1,27 @@
1
+ import { ViewProps } from "react-native";
2
+ import { PHImageContentMode, PHImageRequestOptionsDeliveryMode, PHImageRequestOptionsResizeMode } from "./ExpoPhotos.types";
3
+ export interface PHImageLoadEvent {
4
+ size: {
5
+ width: number;
6
+ height: number;
7
+ };
8
+ scale: number;
9
+ }
10
+ export interface PHImageErrorEvent {
11
+ message: string;
12
+ }
13
+ export interface PHImageProps extends ViewProps {
14
+ localIdentifier: string;
15
+ isNetworkAccessAllowed?: boolean;
16
+ deliveryMode?: PHImageRequestOptionsDeliveryMode;
17
+ resizeMode?: PHImageRequestOptionsResizeMode;
18
+ contentMode?: PHImageContentMode;
19
+ onLoad?: (event: {
20
+ nativeEvent: PHImageLoadEvent;
21
+ }) => void;
22
+ onError?: (event: {
23
+ nativeEvent: PHImageErrorEvent;
24
+ }) => void;
25
+ }
26
+ export declare function PHImage(props: PHImageProps): import("react").JSX.Element;
27
+ //# sourceMappingURL=PHImage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PHImage.d.ts","sourceRoot":"","sources":["../src/PHImage.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EACL,kBAAkB,EAClB,iCAAiC,EACjC,+BAA+B,EAChC,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,eAAe,EAAE,MAAM,CAAC;IACxB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,YAAY,CAAC,EAAE,iCAAiC,CAAC;IACjD,UAAU,CAAC,EAAE,+BAA+B,CAAC;IAC7C,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,gBAAgB,CAAA;KAAE,KAAK,IAAI,CAAC;IAC5D,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,iBAAiB,CAAA;KAAE,KAAK,IAAI,CAAC;CAC/D;AAID,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,+BAE1C"}
@@ -0,0 +1,6 @@
1
+ import { requireNativeViewManager } from "expo-modules-core";
2
+ const NativePHImageView = requireNativeViewManager("ExpoPhotos", "PHImageView");
3
+ export function PHImage(props) {
4
+ return <NativePHImageView {...props}/>;
5
+ }
6
+ //# sourceMappingURL=PHImage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PHImage.js","sourceRoot":"","sources":["../src/PHImage.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AA2B7D,MAAM,iBAAiB,GAAG,wBAAwB,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;AAEhF,MAAM,UAAU,OAAO,CAAC,KAAmB;IACzC,OAAO,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,EAAG,CAAC;AAC1C,CAAC","sourcesContent":["import { requireNativeViewManager } from \"expo-modules-core\";\nimport { ViewProps } from \"react-native\";\nimport {\n PHImageContentMode,\n PHImageRequestOptionsDeliveryMode,\n PHImageRequestOptionsResizeMode,\n} from \"./ExpoPhotos.types\";\n\nexport interface PHImageLoadEvent {\n size: { width: number; height: number };\n scale: number;\n}\n\nexport interface PHImageErrorEvent {\n message: string;\n}\n\nexport interface PHImageProps extends ViewProps {\n localIdentifier: string;\n isNetworkAccessAllowed?: boolean;\n deliveryMode?: PHImageRequestOptionsDeliveryMode;\n resizeMode?: PHImageRequestOptionsResizeMode;\n contentMode?: PHImageContentMode;\n onLoad?: (event: { nativeEvent: PHImageLoadEvent }) => void;\n onError?: (event: { nativeEvent: PHImageErrorEvent }) => void;\n}\n\nconst NativePHImageView = requireNativeViewManager(\"ExpoPhotos\", \"PHImageView\");\n\nexport function PHImage(props: PHImageProps) {\n return <NativePHImageView {...props} />;\n}\n"]}
package/build/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { default } from "./ExpoPhotosModule";
2
2
  export * from "./ExpoPhotos.types";
3
- export { createPHImageSource } from "./PHImageSource";
4
- export type { PHImageSourceOptions } from "./PHImageSource";
3
+ export { PHImage } from "./PHImage";
4
+ export type { PHImageProps, PHImageLoadEvent, PHImageErrorEvent } from "./PHImage";
5
5
  export { PHVideo } from "./PHVideo";
6
6
  export type { PHVideoProps } from "./PHVideo";
7
7
  //# 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;AACnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,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,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACnF,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC"}
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { default } from "./ExpoPhotosModule";
2
2
  export * from "./ExpoPhotos.types";
3
- export { createPHImageSource } from "./PHImageSource";
3
+ export { PHImage } from "./PHImage";
4
4
  export { PHVideo } from "./PHVideo";
5
5
  //# 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;AACnC,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC","sourcesContent":["export { default } from \"./ExpoPhotosModule\";\nexport * from \"./ExpoPhotos.types\";\nexport { createPHImageSource } from \"./PHImageSource\";\nexport type { PHImageSourceOptions } from \"./PHImageSource\";\nexport { PHVideo } from \"./PHVideo\";\nexport type { PHVideoProps } from \"./PHVideo\";\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,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC","sourcesContent":["export { default } from \"./ExpoPhotosModule\";\nexport * from \"./ExpoPhotos.types\";\nexport { PHImage } from \"./PHImage\";\nexport type { PHImageProps, PHImageLoadEvent, PHImageErrorEvent } from \"./PHImage\";\nexport { PHVideo } from \"./PHVideo\";\nexport type { PHVideoProps } from \"./PHVideo\";\n"]}
@@ -14,11 +14,10 @@ Pod::Spec.new do |s|
14
14
  s.swift_version = '5.9'
15
15
  s.source = { git: package['repository'] || 'https://github.com/hortemo/expo-photos' }
16
16
  s.static_framework = true
17
+ s.frameworks = 'Photos','PhotosUI'
17
18
 
18
19
  s.dependency 'ExpoModulesCore'
19
- s.dependency 'React-Core'
20
20
  s.dependency 'SDWebImageWebPCoder', '~> 0.14'
21
21
 
22
22
  s.source_files = '**/*.{h,m,swift}'
23
- s.requires_arc = true
24
23
  end
@@ -305,6 +305,30 @@ public final class ExpoPhotos: Module {
305
305
  }
306
306
  }
307
307
 
308
+ View(PHImageView.self) {
309
+ Prop("localIdentifier") { (view: PHImageView, localIdentifier: String?) in
310
+ view.localIdentifier = localIdentifier
311
+ }
312
+
313
+ Prop("isNetworkAccessAllowed") { (view: PHImageView, isNetworkAccessAllowed: Bool?) in
314
+ view.isNetworkAccessAllowed = isNetworkAccessAllowed ?? true
315
+ }
316
+
317
+ Prop("deliveryMode") { (view: PHImageView, deliveryMode: PHImageRequestOptionsDeliveryMode?) in
318
+ view.deliveryMode = deliveryMode ?? .opportunistic
319
+ }
320
+
321
+ Prop("resizeMode") { (view: PHImageView, resizeMode: PHImageRequestOptionsResizeMode?) in
322
+ view.resizeMode = resizeMode ?? .none
323
+ }
324
+
325
+ Prop("contentMode") { (view: PHImageView, contentMode: PHImageContentMode?) in
326
+ view.imageContentMode = contentMode ?? .aspectFill
327
+ }
328
+
329
+ Events("onLoad", "onError")
330
+ }
331
+
308
332
  View(PHVideoView.self) {
309
333
  Prop("localIdentifier") { (view: PHVideoView, localIdentifier: String?) in
310
334
  view.localIdentifier = localIdentifier
@@ -0,0 +1,137 @@
1
+ import ExpoModulesCore
2
+ import Photos
3
+ import UIKit
4
+
5
+ public final class PHImageView: ExpoView {
6
+ let onLoad = EventDispatcher()
7
+ let onError = EventDispatcher()
8
+
9
+ private var currentRequestId: PHImageRequestID?
10
+ private var imageView: UIImageView
11
+
12
+ var localIdentifier: String? {
13
+ didSet {
14
+ if localIdentifier != oldValue {
15
+ loadImage()
16
+ }
17
+ }
18
+ }
19
+
20
+ var isNetworkAccessAllowed: Bool = true {
21
+ didSet {
22
+ if isNetworkAccessAllowed != oldValue {
23
+ loadImage()
24
+ }
25
+ }
26
+ }
27
+
28
+ var deliveryMode: PHImageRequestOptionsDeliveryMode = .opportunistic {
29
+ didSet {
30
+ if deliveryMode != oldValue {
31
+ loadImage()
32
+ }
33
+ }
34
+ }
35
+
36
+ var resizeMode: PHImageRequestOptionsResizeMode = .none {
37
+ didSet {
38
+ if resizeMode != oldValue {
39
+ loadImage()
40
+ }
41
+ }
42
+ }
43
+
44
+ var imageContentMode: PHImageContentMode = .aspectFill {
45
+ didSet {
46
+ if imageContentMode != oldValue {
47
+ imageView.contentMode = imageContentMode == .aspectFit ? .scaleAspectFit : .scaleAspectFill
48
+ loadImage()
49
+ }
50
+ }
51
+ }
52
+
53
+ public required init(appContext: AppContext? = nil) {
54
+ imageView = UIImageView()
55
+ super.init(appContext: appContext)
56
+ imageView.clipsToBounds = true
57
+ imageView.contentMode = imageContentMode == .aspectFit ? .scaleAspectFit : .scaleAspectFill
58
+ addSubview(imageView)
59
+ }
60
+
61
+ public override func layoutSubviews() {
62
+ super.layoutSubviews()
63
+ imageView.frame = bounds
64
+ loadImage()
65
+ }
66
+
67
+ public override func willMove(toWindow newWindow: UIWindow?) {
68
+ super.willMove(toWindow: newWindow)
69
+ if newWindow == nil {
70
+ cleanup()
71
+ }
72
+ }
73
+
74
+ private func loadImage() {
75
+ cleanup()
76
+
77
+ guard let localIdentifier = localIdentifier else { return }
78
+ guard bounds.width > 0 && bounds.height > 0 else { return }
79
+
80
+ guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil).firstObject else {
81
+ onError(["message": "Asset not found for identifier: \(localIdentifier)"])
82
+ return
83
+ }
84
+
85
+ let options = PHImageRequestOptions()
86
+ options.isNetworkAccessAllowed = isNetworkAccessAllowed
87
+ options.deliveryMode = deliveryMode
88
+ options.resizeMode = resizeMode
89
+
90
+ let scale = window?.screen.scale ?? UIScreen.main.scale
91
+ let targetSize = CGSize(width: bounds.width * scale, height: bounds.height * scale)
92
+
93
+ currentRequestId = PHImageManager.default().requestImage(
94
+ for: asset,
95
+ targetSize: targetSize,
96
+ contentMode: imageContentMode,
97
+ options: options
98
+ ) { [weak self] image, info in
99
+ guard let self = self else { return }
100
+
101
+ DispatchQueue.main.async {
102
+ let isCancelled = (info?[PHImageCancelledKey] as? NSNumber)?.boolValue ?? false
103
+ if isCancelled { return }
104
+
105
+ if let error = info?[PHImageErrorKey] as? Error {
106
+ self.onError(["message": error.localizedDescription])
107
+ return
108
+ }
109
+
110
+ guard let image = image else {
111
+ self.onError(["message": "Failed to load image"])
112
+ return
113
+ }
114
+
115
+ self.imageView.image = image
116
+
117
+ let isDegraded = (info?[PHImageResultIsDegradedKey] as? NSNumber)?.boolValue ?? false
118
+ if !isDegraded {
119
+ self.onLoad([
120
+ "size": [
121
+ "width": image.size.width,
122
+ "height": image.size.height,
123
+ ],
124
+ "scale": image.scale,
125
+ ])
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ private func cleanup() {
132
+ if let requestId = currentRequestId {
133
+ PHImageManager.default().cancelImageRequest(requestId)
134
+ currentRequestId = nil
135
+ }
136
+ }
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hortemo/expo-photos",
3
- "version": "6.0.0",
3
+ "version": "8.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",
@@ -38,15 +38,5 @@
38
38
  "peerDependencies": {
39
39
  "expo": "*",
40
40
  "react-native": "*"
41
- },
42
- "codegenConfig": {
43
- "name": "expo-photos",
44
- "type": "modules",
45
- "jsSrcsDir": "./src",
46
- "ios": {
47
- "modulesConformingToProtocol": {
48
- "RCTImageURLLoader": "ExpoPhotosImageLoader"
49
- }
50
- }
51
41
  }
52
42
  }
@@ -0,0 +1,32 @@
1
+ import { requireNativeViewManager } from "expo-modules-core";
2
+ import { ViewProps } from "react-native";
3
+ import {
4
+ PHImageContentMode,
5
+ PHImageRequestOptionsDeliveryMode,
6
+ PHImageRequestOptionsResizeMode,
7
+ } from "./ExpoPhotos.types";
8
+
9
+ export interface PHImageLoadEvent {
10
+ size: { width: number; height: number };
11
+ scale: number;
12
+ }
13
+
14
+ export interface PHImageErrorEvent {
15
+ message: string;
16
+ }
17
+
18
+ export interface PHImageProps extends ViewProps {
19
+ localIdentifier: string;
20
+ isNetworkAccessAllowed?: boolean;
21
+ deliveryMode?: PHImageRequestOptionsDeliveryMode;
22
+ resizeMode?: PHImageRequestOptionsResizeMode;
23
+ contentMode?: PHImageContentMode;
24
+ onLoad?: (event: { nativeEvent: PHImageLoadEvent }) => void;
25
+ onError?: (event: { nativeEvent: PHImageErrorEvent }) => void;
26
+ }
27
+
28
+ const NativePHImageView = requireNativeViewManager("ExpoPhotos", "PHImageView");
29
+
30
+ export function PHImage(props: PHImageProps) {
31
+ return <NativePHImageView {...props} />;
32
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { default } from "./ExpoPhotosModule";
2
2
  export * from "./ExpoPhotos.types";
3
- export { createPHImageSource } from "./PHImageSource";
4
- export type { PHImageSourceOptions } from "./PHImageSource";
3
+ export { PHImage } from "./PHImage";
4
+ export type { PHImageProps, PHImageLoadEvent, PHImageErrorEvent } from "./PHImage";
5
5
  export { PHVideo } from "./PHVideo";
6
6
  export type { PHVideoProps } from "./PHVideo";
@@ -1,32 +0,0 @@
1
- import type { ImageSourcePropType } from "react-native";
2
- import { PHImageContentMode, PHImageRequestOptionsDeliveryMode, PHImageRequestOptionsResizeMode, RequestImageTargetSize } from "./ExpoPhotos.types";
3
- export interface PHImageSourceOptions {
4
- localIdentifier: string;
5
- targetSize?: RequestImageTargetSize;
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;
32
- //# sourceMappingURL=PHImageSource.d.ts.map
@@ -1 +0,0 @@
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,kBAAkB,EAClB,iCAAiC,EACjC,+BAA+B,EAC/B,sBAAsB,EACvB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,oBAAoB;IACnC,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,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,CAwBrB"}
@@ -1,46 +0,0 @@
1
- const buildQueryString = (params) => {
2
- const entries = Object.entries(params).filter(([, value]) => value !== undefined);
3
- if (entries.length === 0) {
4
- return "";
5
- }
6
- return ("?" +
7
- entries
8
- .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value ?? "")}`)
9
- .join("&"));
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 function createPHImageSource(options) {
32
- const { localIdentifier, resizeMode, deliveryMode, contentMode, isNetworkAccessAllowed, } = options;
33
- const targetSize = options.targetSize && typeof options.targetSize === "object"
34
- ? `${options.targetSize.width}x${options.targetSize.height}`
35
- : options.targetSize;
36
- const query = buildQueryString({
37
- localIdentifier,
38
- targetSize,
39
- resizeMode,
40
- deliveryMode,
41
- contentMode,
42
- isNetworkAccessAllowed,
43
- });
44
- return { uri: `expo-photos://${query}` };
45
- }
46
- //# sourceMappingURL=PHImageSource.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"PHImageSource.js","sourceRoot":"","sources":["../src/PHImageSource.tsx"],"names":[],"mappings":"AAiBA,MAAM,gBAAgB,GAAG,CACvB,MAA6D,EACrD,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,EACJ,eAAe,EACf,UAAU,EACV,YAAY,EACZ,WAAW,EACX,sBAAsB,GACvB,GAAG,OAAO,CAAC;IAEZ,MAAM,UAAU,GACd,OAAO,CAAC,UAAU,IAAI,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ;QAC1D,CAAC,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE;QAC5D,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;IAEzB,MAAM,KAAK,GAAG,gBAAgB,CAAC;QAC7B,eAAe;QACf,UAAU;QACV,UAAU;QACV,YAAY;QACZ,WAAW;QACX,sBAAsB;KACvB,CAAC,CAAC;IAEH,OAAO,EAAE,GAAG,EAAE,iBAAiB,KAAK,EAAE,EAAE,CAAC;AAC3C,CAAC","sourcesContent":["import type { ImageSourcePropType } from \"react-native\";\nimport {\n PHImageContentMode,\n PHImageRequestOptionsDeliveryMode,\n PHImageRequestOptionsResizeMode,\n RequestImageTargetSize,\n} from \"./ExpoPhotos.types\";\n\nexport interface PHImageSourceOptions {\n localIdentifier: string;\n targetSize?: RequestImageTargetSize;\n resizeMode?: PHImageRequestOptionsResizeMode;\n deliveryMode?: PHImageRequestOptionsDeliveryMode;\n contentMode?: PHImageContentMode;\n isNetworkAccessAllowed?: boolean;\n}\n\nconst buildQueryString = (\n params: Record<string, string | number | boolean | 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 {\n const {\n localIdentifier,\n resizeMode,\n deliveryMode,\n contentMode,\n isNetworkAccessAllowed,\n } = options;\n\n const targetSize =\n options.targetSize && typeof options.targetSize === \"object\"\n ? `${options.targetSize.width}x${options.targetSize.height}`\n : options.targetSize;\n\n const query = buildQueryString({\n localIdentifier,\n targetSize,\n resizeMode,\n deliveryMode,\n contentMode,\n isNetworkAccessAllowed,\n });\n\n return { uri: `expo-photos://${query}` };\n}\n"]}
@@ -1,187 +0,0 @@
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
- if targetSizeString.caseInsensitiveCompare("PHImageManagerMaximumSize") == .orderedSame {
78
- targetSize = PHImageManagerMaximumSize
79
- } else {
80
- let components = targetSizeString.lowercased().components(separatedBy: "x")
81
- guard components.count == 2,
82
- let width = Double(components[0]),
83
- let height = Double(components[1]) else {
84
- completionHandler(RCTErrorWithMessage("Invalid targetSize format. Expected WIDTHxHEIGHT or PHImageManagerMaximumSize."), nil)
85
- return {}
86
- }
87
- targetSize = CGSize(width: width, height: height)
88
- }
89
- }
90
-
91
- if let deliveryModeString = queryParams["deliveryMode"] {
92
- guard let deliveryMode = PHImageRequestOptionsDeliveryMode.from(queryParam: deliveryModeString) else {
93
- completionHandler(RCTErrorWithMessage("Invalid deliveryMode value."), nil)
94
- return {}
95
- }
96
- imageOptions.deliveryMode = deliveryMode
97
- }
98
-
99
- if let resizeModeString = queryParams["resizeMode"] {
100
- guard let resizeMode = PHImageRequestOptionsResizeMode.from(queryParam: resizeModeString) else {
101
- completionHandler(RCTErrorWithMessage("Invalid resizeMode value."), nil)
102
- return {}
103
- }
104
- imageOptions.resizeMode = resizeMode
105
- }
106
-
107
- if let isNetworkAccessAllowedString = queryParams["isNetworkAccessAllowed"] {
108
- imageOptions.isNetworkAccessAllowed = Bool.from(queryParam: isNetworkAccessAllowedString)
109
- }
110
-
111
- var contentMode: PHImageContentMode = .default
112
- if let contentModeString = queryParams["contentMode"] {
113
- guard let _contentMode = PHImageContentMode.from(queryParam: contentModeString) else {
114
- completionHandler(RCTErrorWithMessage("Invalid contentMode value."), nil)
115
- return {}
116
- }
117
- contentMode = _contentMode
118
- }
119
-
120
- if let progressHandler = progressHandler {
121
- imageOptions.progressHandler = { progress, error, stop, info in
122
- let multiplier: Int64 = 1_000_000
123
- progressHandler(Int64(progress * Double(multiplier)), multiplier)
124
- }
125
- }
126
-
127
- let requestID = PHImageManager.default().requestImage(
128
- for: asset,
129
- targetSize: targetSize,
130
- contentMode: contentMode,
131
- options: imageOptions
132
- ) { result, info in
133
- guard let info = info else {
134
- return
135
- }
136
-
137
- let isCancelled = (info[PHImageCancelledKey] as? NSNumber)?.boolValue ?? false
138
- let isDegraded = (info[PHImageResultIsDegradedKey] as? NSNumber)?.boolValue ?? false
139
-
140
- if isCancelled {
141
- return
142
- }
143
-
144
- if let result = result {
145
- if isDegraded {
146
- partialLoadHandler?(result)
147
- } else {
148
- completionHandler(nil, result)
149
- }
150
- } else if let error = info[PHImageErrorKey] as? Error {
151
- completionHandler(error, nil)
152
- }
153
- }
154
-
155
- return {
156
- PHImageManager.default().cancelImageRequest(requestID)
157
- }
158
- }
159
- }
160
-
161
- // MARK: - RawRepresentable Extension
162
-
163
- extension RawRepresentable where RawValue == Int {
164
- static func from(queryParam string: String?) -> Self? {
165
- guard let string = string, !string.isEmpty else {
166
- return nil
167
- }
168
- guard let intValue = Int(string) else {
169
- return nil
170
- }
171
- return Self(rawValue: intValue)
172
- }
173
- }
174
-
175
- // MARK: - Bool Extension
176
-
177
- extension Bool {
178
- static func from(queryParam string: String?) -> Bool {
179
- guard let string = string, !string.isEmpty else {
180
- return false
181
- }
182
- return string.lowercased() == "true"
183
- }
184
- }
185
-
186
- #endif
187
-
@@ -1,86 +0,0 @@
1
- import type { ImageSourcePropType } from "react-native";
2
- import {
3
- PHImageContentMode,
4
- PHImageRequestOptionsDeliveryMode,
5
- PHImageRequestOptionsResizeMode,
6
- RequestImageTargetSize,
7
- } from "./ExpoPhotos.types";
8
-
9
- export interface PHImageSourceOptions {
10
- localIdentifier: string;
11
- targetSize?: RequestImageTargetSize;
12
- resizeMode?: PHImageRequestOptionsResizeMode;
13
- deliveryMode?: PHImageRequestOptionsDeliveryMode;
14
- contentMode?: PHImageContentMode;
15
- isNetworkAccessAllowed?: boolean;
16
- }
17
-
18
- const buildQueryString = (
19
- params: Record<string, string | number | boolean | 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 {
63
- const {
64
- localIdentifier,
65
- resizeMode,
66
- deliveryMode,
67
- contentMode,
68
- isNetworkAccessAllowed,
69
- } = options;
70
-
71
- const targetSize =
72
- options.targetSize && typeof options.targetSize === "object"
73
- ? `${options.targetSize.width}x${options.targetSize.height}`
74
- : options.targetSize;
75
-
76
- const query = buildQueryString({
77
- localIdentifier,
78
- targetSize,
79
- resizeMode,
80
- deliveryMode,
81
- contentMode,
82
- isNetworkAccessAllowed,
83
- });
84
-
85
- return { uri: `expo-photos://${query}` };
86
- }