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.
- package/ios/HybridPictureSelector.swift +147 -134
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
38
|
+
let promise = Promise<[MediaAsset]>()
|
|
39
|
+
|
|
40
|
+
DispatchQueue.main.async { [weak self] in
|
|
51
41
|
guard let self else {
|
|
52
|
-
|
|
42
|
+
promise.reject(withError: PictureSelectorError.unknown("openPicker: native module was deallocated"))
|
|
53
43
|
return
|
|
54
44
|
}
|
|
55
45
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
76
|
+
let promise = Promise<[MediaAsset]>()
|
|
77
|
+
|
|
78
|
+
DispatchQueue.main.async { [weak self] in
|
|
95
79
|
guard let self else {
|
|
96
|
-
|
|
80
|
+
promise.reject(withError: PictureSelectorError.unknown("openCamera: native module was deallocated"))
|
|
97
81
|
return
|
|
98
82
|
}
|
|
99
83
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
91
|
+
guard let topVC = self.topViewController() else {
|
|
92
|
+
promise.reject(withError: PictureSelectorError.unknown("No active UIViewController."))
|
|
93
|
+
return
|
|
94
|
+
}
|
|
112
95
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
96
|
+
var cameraConfig = CameraConfiguration()
|
|
97
|
+
if let maxDur = options.maxVideoDuration {
|
|
98
|
+
cameraConfig.videoMaximumDuration = maxDur
|
|
99
|
+
}
|
|
118
100
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
194
|
-
cropSizeConfig.isRoundCrop = true
|
|
170
|
+
editorConfig.cropSize.isRoundCrop = true
|
|
195
171
|
} else if crop.freeStyle == true {
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
273
|
-
|
|
274
|
-
let
|
|
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.
|
|
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.
|
|
378
|
+
s.promise.resolve(withResult: assets)
|
|
363
379
|
} catch {
|
|
364
|
-
s.
|
|
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?.
|
|
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