react-native-picture-selector 1.0.27 → 1.0.29

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.
@@ -1,5 +1,6 @@
1
1
  import Foundation
2
2
  import UIKit
3
+ import AVFoundation
3
4
  import HXPhotoPicker
4
5
  import NitroModules
5
6
 
@@ -15,27 +16,14 @@ import NitroModules
15
16
  // - Nitro calls openPicker / openCamera on the JS thread.
16
17
  // - All UIKit calls are dispatched to DispatchQueue.main.
17
18
  // - Async result mapping runs in a Swift Task (cooperative thread pool).
18
- //
19
- // API REQUIRES VERIFICATION:
20
- // - PhotoPickerControllerDelegate method signatures in HXPhotoPicker v5.0.5.
21
- // - PhotoAsset.getURL(compression:result:) callback API availability.
22
- // - PickerResult.photoAssets field name.
23
- // - PhotoAsset.mediaType enum values (.photo / .video).
24
- // - PhotoAsset.imageSize property name.
25
- // - PhotoAsset.videoDuration unit (seconds vs ms).
26
- // - AssetURLResult.fileSize field name / optionality.
27
- // - PhotoAsset.Compression type name and initialiser parameters.
28
- // - EditorConfiguration.Photo.CropSize.isRoundCrop property name.
29
19
  // ─────────────────────────────────────────────────────────────────────────────
30
20
 
31
21
  final class HybridPictureSelector: HybridHybridPictureSelectorSpec_base, HybridHybridPictureSelectorSpec_protocol {
32
22
 
33
23
  // MARK: - Private state
34
24
 
35
- /// Bundles the pending resolver together with the options so the delegate
36
- /// can perform compression-aware result mapping.
37
25
  private struct PendingSession {
38
- let resolver: (Result<[MediaAsset], Error>) -> Void
26
+ let promise: Promise<[MediaAsset]>
39
27
  let options: PictureSelectorOptions
40
28
  }
41
29
 
@@ -47,110 +35,100 @@ final class HybridPictureSelector: HybridHybridPictureSelectorSpec_base, HybridH
47
35
  // MARK: - openPicker
48
36
 
49
37
  func openPicker(options: PictureSelectorOptions) -> Promise<[MediaAsset]> {
50
- return Promise { [weak self] resolver in
38
+ let promise = Promise<[MediaAsset]>()
39
+
40
+ DispatchQueue.main.async { [weak self] in
51
41
  guard let self else {
52
- resolver.reject(PictureSelectorError.unknown("openPicker: native module was deallocated"))
42
+ promise.reject(withError: PictureSelectorError.unknown("openPicker: native module was deallocated"))
53
43
  return
54
44
  }
55
45
 
56
- DispatchQueue.main.async {
57
- if self.session != nil {
58
- resolver.reject(PictureSelectorError.unknown(
59
- "A picker or camera session is already active. Dismiss it before opening a new one."
60
- ))
61
- return
62
- }
63
-
64
- guard let topVC = self.topViewController() else {
65
- resolver.reject(PictureSelectorError.unknown(
66
- "No active UIViewController. Ensure the picker is called from a mounted component."
67
- ))
68
- return
69
- }
46
+ if self.session != nil {
47
+ promise.reject(withError: PictureSelectorError.unknown(
48
+ "A picker or camera session is already active. Dismiss it before opening a new one."
49
+ ))
50
+ return
51
+ }
70
52
 
71
- self.session = PendingSession(
72
- resolver: { result in
73
- switch result {
74
- case .success(let assets): resolver.resolve(assets)
75
- case .failure(let err): resolver.reject(err)
76
- }
77
- },
78
- options: options
79
- )
80
-
81
- let config = self.buildPickerConfig(from: options)
82
- let picker = PhotoPickerController(picker: config)
83
- picker.pickerDelegate = self
84
-
85
- self.activePicker = picker
86
- topVC.present(picker, animated: true)
53
+ guard let topVC = self.topViewController() else {
54
+ promise.reject(withError: PictureSelectorError.unknown(
55
+ "No active UIViewController. Ensure the picker is called from a mounted component."
56
+ ))
57
+ return
87
58
  }
59
+
60
+ self.session = PendingSession(promise: promise, options: options)
61
+
62
+ let config = self.buildPickerConfig(from: options)
63
+ let picker = PhotoPickerController(picker: config)
64
+ picker.pickerDelegate = self
65
+
66
+ self.activePicker = picker
67
+ topVC.present(picker, animated: true)
88
68
  }
69
+
70
+ return promise
89
71
  }
90
72
 
91
73
  // MARK: - openCamera
92
74
 
93
75
  func openCamera(options: PictureSelectorOptions) -> Promise<[MediaAsset]> {
94
- return Promise { [weak self] resolver in
76
+ let promise = Promise<[MediaAsset]>()
77
+
78
+ DispatchQueue.main.async { [weak self] in
95
79
  guard let self else {
96
- resolver.reject(PictureSelectorError.unknown("openCamera: native module was deallocated"))
80
+ promise.reject(withError: PictureSelectorError.unknown("openCamera: native module was deallocated"))
97
81
  return
98
82
  }
99
83
 
100
- DispatchQueue.main.async {
101
- if self.session != nil {
102
- resolver.reject(PictureSelectorError.unknown(
103
- "A picker or camera session is already active. Dismiss it before opening a new one."
104
- ))
105
- return
106
- }
84
+ if self.session != nil {
85
+ promise.reject(withError: PictureSelectorError.unknown(
86
+ "A picker or camera session is already active. Dismiss it before opening a new one."
87
+ ))
88
+ return
89
+ }
107
90
 
108
- guard let topVC = self.topViewController() else {
109
- resolver.reject(PictureSelectorError.unknown("No active UIViewController."))
110
- return
111
- }
91
+ guard let topVC = self.topViewController() else {
92
+ promise.reject(withError: PictureSelectorError.unknown("No active UIViewController."))
93
+ return
94
+ }
112
95
 
113
- var cameraConfig = CameraConfiguration()
114
- cameraConfig.mediaType = (options.mediaType == .video) ? .video : .photo
115
- if let maxDur = options.maxVideoDuration {
116
- cameraConfig.videoMaximumDuration = maxDur
117
- }
96
+ var cameraConfig = CameraConfiguration()
97
+ if let maxDur = options.maxVideoDuration {
98
+ cameraConfig.videoMaximumDuration = maxDur
99
+ }
118
100
 
119
- // API REQUIRES VERIFICATION:
120
- // Photo.camera(_:fromViewController:completion:cancel:) method signature.
121
- // If this overload doesn't exist, use CameraController directly:
122
- // let cam = CameraController(config: cameraConfig)
123
- // cam.onCompletion = { ... }
124
- // topVC.present(cam, animated: true)
125
- Photo.camera(
126
- cameraConfig,
127
- fromViewController: topVC
128
- ) { [weak self] result, _ in
129
- guard let self else {
130
- resolver.reject(PictureSelectorError.unknown("openCamera: native module was deallocated"))
131
- return
132
- }
133
- guard let photoAsset = result?.photoAsset else {
134
- resolver.reject(PictureSelectorError.cancelled)
135
- return
136
- }
137
- Task {
138
- do {
139
- let asset = try await self.mapAsset(
140
- photoAsset,
141
- compress: options.compress,
142
- isOriginal: false
143
- )
144
- resolver.resolve([asset])
145
- } catch {
146
- resolver.reject(error)
147
- }
101
+ let captureType: CameraController.CaptureType
102
+ switch options.mediaType {
103
+ case .video: captureType = .video
104
+ case .all: captureType = .all
105
+ default: captureType = .photo
106
+ }
107
+
108
+ let camera = CameraController(config: cameraConfig, type: captureType)
109
+ camera.completion = { [weak self] result, _, _ in
110
+ guard let self else {
111
+ promise.reject(withError: PictureSelectorError.unknown("openCamera: native module was deallocated"))
112
+ return
113
+ }
114
+ Task {
115
+ do {
116
+ let asset = try await self.mapCameraResult(result, compress: options.compress)
117
+ promise.resolve(withResult: [asset])
118
+ } catch {
119
+ promise.reject(withError: error)
148
120
  }
149
- } cancel: { _ in
150
- resolver.reject(PictureSelectorError.cancelled)
151
121
  }
152
122
  }
123
+ camera.cancelHandler = { _ in
124
+ promise.reject(withError: PictureSelectorError.cancelled)
125
+ }
126
+
127
+ self.activePicker = camera
128
+ topVC.present(camera, animated: true)
153
129
  }
130
+
131
+ return promise
154
132
  }
155
133
 
156
134
  // MARK: - Config builder
@@ -172,36 +150,32 @@ final class HybridPictureSelector: HybridHybridPictureSelectorSpec_base, HybridH
172
150
  config.maximumSelectedCount = Int(options.maxCount ?? 1)
173
151
 
174
152
  // In-picker camera button
175
- config.allowCustomCamera = options.enableCamera ?? true
153
+ config.photoList.allowAddCamera = options.enableCamera ?? true
176
154
 
177
- // Video duration limits
155
+ // Video duration limits (Int in HXPhotoPicker)
178
156
  if let maxDur = options.maxVideoDuration {
179
- config.maximumSelectedVideoDuration = maxDur
157
+ config.maximumSelectedVideoDuration = Int(maxDur)
180
158
  }
181
159
  if let minDur = options.minVideoDuration {
182
- config.minimumSelectedVideoDuration = minDur
160
+ config.minimumSelectedVideoDuration = Int(minDur)
183
161
  }
184
162
 
185
- // Editor / crop (only when maxCount == 1)
163
+ // Editor / crop (only when maxCount == 1)
186
164
  let maxCount = Int(options.maxCount ?? 1)
187
165
  if let crop = options.crop, crop.enabled, maxCount == 1 {
188
166
  config.editorOptions = [.photo]
189
167
  var editorConfig = EditorConfiguration()
190
168
 
191
- var cropSizeConfig = EditorConfiguration.Photo.CropSize()
192
169
  if crop.circular == true {
193
- // API REQUIRES VERIFICATION: isRoundCrop property name in v5.0.5
194
- cropSizeConfig.isRoundCrop = true
170
+ editorConfig.cropSize.isRoundCrop = true
195
171
  } else if crop.freeStyle == true {
196
- cropSizeConfig.aspectRatios = [] // empty array = free style
172
+ editorConfig.cropSize.isFixedRatio = false
197
173
  } else {
198
174
  let x = crop.ratioX ?? 1.0
199
175
  let y = crop.ratioY ?? 1.0
200
- cropSizeConfig.aspectRatios = [
201
- .init(title: "", ratio: .init(width: x, height: y))
202
- ]
176
+ editorConfig.cropSize.isFixedRatio = true
177
+ editorConfig.cropSize.aspectRatio = .init(width: x, height: y)
203
178
  }
204
- editorConfig.photo.cropSize = cropSizeConfig
205
179
  config.editor = editorConfig
206
180
  }
207
181
 
@@ -210,13 +184,6 @@ final class HybridPictureSelector: HybridHybridPictureSelectorSpec_base, HybridH
210
184
  config.themeColor = color
211
185
  }
212
186
 
213
- // selectedAssets: pre-selecting assets by file:// URI requires resolving each URI
214
- // back to a PHAsset via PHPhotoLibrary and wrapping it in a PhotoAsset object.
215
- // This is not yet implemented. Callers should not rely on this option on iOS.
216
- // TODO: implement pre-selection using PHAsset.fetchAssets(withALAssetURLs:options:)
217
- // or localIdentifier lookup, then set config.preSelectedAssets (verify field name
218
- // in HXPhotoPicker v5.0.5 before enabling).
219
-
220
187
  return config
221
188
  }
222
189
 
@@ -243,8 +210,6 @@ final class HybridPictureSelector: HybridHybridPictureSelectorSpec_base, HybridH
243
210
  compress: CompressOptions?,
244
211
  isOriginal: Bool
245
212
  ) async throws -> MediaAsset {
246
- // Obtain file URL via callback, bridged to async/await.
247
- // API REQUIRES VERIFICATION: getURL(compression:result:) availability in v5.0.5.
248
213
  let urlResult: AssetURLResult = try await withCheckedThrowingContinuation { cont in
249
214
  photoAsset.getURL(
250
215
  compression: buildCompression(from: compress)
@@ -256,22 +221,23 @@ final class HybridPictureSelector: HybridHybridPictureSelectorSpec_base, HybridH
256
221
  }
257
222
  }
258
223
 
259
- // Determine if the user applied edits
260
224
  let wasEdited = photoAsset.editedResult != nil
261
225
  let finalUri = urlResult.url.absoluteString
262
226
  let editedUri: String? = wasEdited ? finalUri : nil
263
227
 
264
- // Image / video dimensions
265
- // API REQUIRES VERIFICATION: imageSize property name in v5.0.5
266
228
  let size: CGSize = photoAsset.imageSize
267
229
 
268
- // Duration: HXPhotoPicker returns seconds; bridge spec expects ms.
269
- // API REQUIRES VERIFICATION: videoDuration property name and unit.
230
+ // HXPhotoPicker returns seconds; bridge spec expects ms.
270
231
  let durationMs: Double = (photoAsset.videoDuration ?? 0) * 1_000
271
232
 
272
- // File size
273
- // API REQUIRES VERIFICATION: AssetURLResult.fileSize field name.
274
- let fileSize: Double = Double(urlResult.fileSize ?? 0)
233
+ // AssetURLResult has no fileSize; read from disk.
234
+ let fileSize: Double
235
+ if let attrs = try? FileManager.default.attributesOfItem(atPath: urlResult.url.path),
236
+ let bytes = attrs[.size] as? UInt64 {
237
+ fileSize = Double(bytes)
238
+ } else {
239
+ fileSize = 0
240
+ }
275
241
 
276
242
  let typeStr: String = (photoAsset.mediaType == .video) ? "video" : "image"
277
243
 
@@ -290,9 +256,60 @@ final class HybridPictureSelector: HybridHybridPictureSelectorSpec_base, HybridH
290
256
  )
291
257
  }
292
258
 
259
+ // MARK: - Camera result mapping
260
+
261
+ private func mapCameraResult(
262
+ _ result: CameraController.Result,
263
+ compress: CompressOptions?
264
+ ) async throws -> MediaAsset {
265
+ switch result {
266
+ case .image(let image):
267
+ let quality = compress?.quality ?? 0.8
268
+ guard let data = image.jpegData(compressionQuality: quality) else {
269
+ throw PictureSelectorError.unknown("Failed to encode captured image")
270
+ }
271
+ let fileName = "camera_\(UUID().uuidString).jpg"
272
+ let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
273
+ try data.write(to: tempURL)
274
+ let fileSize = Double(data.count)
275
+ return MediaAsset(
276
+ uri: tempURL.absoluteString,
277
+ type: "image",
278
+ mimeType: "image/jpeg",
279
+ width: Double(image.size.width * image.scale),
280
+ height: Double(image.size.height * image.scale),
281
+ duration: 0,
282
+ fileName: fileName,
283
+ fileSize: fileSize,
284
+ editedUri: nil,
285
+ isOriginal: false,
286
+ bucketName: nil
287
+ )
288
+ case .video(let url):
289
+ let attrs = try? FileManager.default.attributesOfItem(atPath: url.path)
290
+ let fileSize = Double((attrs?[.size] as? UInt64) ?? 0)
291
+ let asset = AVURLAsset(url: url)
292
+ let duration = CMTimeGetSeconds(asset.duration) * 1_000
293
+ let tracks = asset.tracks(withMediaType: .video)
294
+ let size = tracks.first?.naturalSize ?? .zero
295
+ return MediaAsset(
296
+ uri: url.absoluteString,
297
+ type: "video",
298
+ mimeType: mimeType(for: url),
299
+ width: Double(size.width),
300
+ height: Double(size.height),
301
+ duration: duration,
302
+ fileName: url.lastPathComponent,
303
+ fileSize: fileSize,
304
+ editedUri: nil,
305
+ isOriginal: false,
306
+ bucketName: nil
307
+ )
308
+ }
309
+ }
310
+
293
311
  // MARK: - Compression helper
294
312
 
295
- /// API REQUIRES VERIFICATION: PhotoAsset.Compression type name and init params in v5.0.5.
296
313
  private func buildCompression(from options: CompressOptions?) -> PhotoAsset.Compression? {
297
314
  guard let opts = options, opts.enabled else { return nil }
298
315
  return PhotoAsset.Compression(
@@ -345,7 +362,6 @@ extension HybridPictureSelector: PhotoPickerControllerDelegate {
345
362
  _ pickerController: PhotoPickerController,
346
363
  didFinishSelection result: PickerResult
347
364
  ) {
348
- // Capture and clear session atomically before dismiss completes
349
365
  let captured = session
350
366
  session = nil
351
367
  activePicker = nil
@@ -353,15 +369,15 @@ extension HybridPictureSelector: PhotoPickerControllerDelegate {
353
369
  pickerController.dismiss(animated: true) { [weak self] in
354
370
  guard let s = captured else { return }
355
371
  guard let self else {
356
- s.resolver(.failure(PictureSelectorError.unknown("pickerController: native module was deallocated")))
372
+ s.promise.reject(withError: PictureSelectorError.unknown("pickerController: native module was deallocated"))
357
373
  return
358
374
  }
359
375
  Task {
360
376
  do {
361
377
  let assets = try await self.mapResults(result, compress: s.options.compress)
362
- s.resolver(.success(assets))
378
+ s.promise.resolve(withResult: assets)
363
379
  } catch {
364
- s.resolver(.failure(error))
380
+ s.promise.reject(withError: error)
365
381
  }
366
382
  }
367
383
  }
@@ -373,7 +389,7 @@ extension HybridPictureSelector: PhotoPickerControllerDelegate {
373
389
  activePicker = nil
374
390
 
375
391
  pickerController.dismiss(animated: true) {
376
- captured?.resolver(.failure(PictureSelectorError.cancelled))
392
+ captured?.promise.reject(withError: PictureSelectorError.cancelled)
377
393
  }
378
394
  }
379
395
  }
@@ -397,9 +413,6 @@ enum PictureSelectorError: Error, LocalizedError {
397
413
  // MARK: - Nitro registration helper
398
414
 
399
415
  /// Called from NitroPictureSelectorOnLoad.mm at startup.
400
- /// Creates a HybridPictureSelector instance and returns a retained raw pointer
401
- /// to its HybridHybridPictureSelectorSpec_cxx wrapper.
402
- /// The caller (C++ factory) takes ownership via create_std__shared_ptr_HybridHybridPictureSelectorSpec_.
403
416
  @_cdecl("NitroPictureSelectorMakeHybrid")
404
417
  public func NitroPictureSelectorMakeHybrid() -> UnsafeMutableRawPointer {
405
418
  HybridPictureSelector().getCxxWrapper().toUnsafe()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-picture-selector",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "High-performance photo/video picker for React Native using Nitro Modules",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",