react-native-picture-selector 1.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 +627 -0
- package/android/CMakeLists.txt +30 -0
- package/android/build.gradle +79 -0
- package/android/proguard-rules.pro +21 -0
- package/android/src/main/AndroidManifest.xml +39 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/GlideEngine.kt +80 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/HybridPictureSelector.kt +138 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/LubanCompressEngine.kt +58 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/MediaAssetMapper.kt +69 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/NitroPictureSelectorPackage.kt +52 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/PictureSelectorOptionsMapper.kt +105 -0
- package/android/src/main/kotlin/com/margelo/pictureselector/UCropEngine.kt +57 -0
- package/android/src/main/res/xml/file_paths.xml +8 -0
- package/ios/HybridPictureSelector.swift +386 -0
- package/ios/NitroPictureSelector.podspec +39 -0
- package/lib/commonjs/PictureSelector.js +74 -0
- package/lib/commonjs/PictureSelector.js.map +1 -0
- package/lib/commonjs/index.js +39 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/PictureSelector.nitro.js +34 -0
- package/lib/commonjs/specs/PictureSelector.nitro.js.map +1 -0
- package/lib/commonjs/types.js +44 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/usePictureSelector.js +122 -0
- package/lib/commonjs/usePictureSelector.js.map +1 -0
- package/lib/module/PictureSelector.js +71 -0
- package/lib/module/PictureSelector.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/specs/PictureSelector.nitro.js +36 -0
- package/lib/module/specs/PictureSelector.nitro.js.map +1 -0
- package/lib/module/types.js +29 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/usePictureSelector.js +119 -0
- package/lib/module/usePictureSelector.js.map +1 -0
- package/lib/typescript/PictureSelector.d.ts +23 -0
- package/lib/typescript/PictureSelector.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +6 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/specs/PictureSelector.nitro.d.ts +96 -0
- package/lib/typescript/specs/PictureSelector.nitro.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +16 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/usePictureSelector.d.ts +26 -0
- package/lib/typescript/usePictureSelector.d.ts.map +1 -0
- package/nitro.json +11 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroPictureSelector+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroPictureSelector+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroPictureSelectorOnLoad.cpp +41 -0
- package/nitrogen/generated/android/NitroPictureSelectorOnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JCompressOptions.hpp +69 -0
- package/nitrogen/generated/android/c++/JCropOptions.hpp +73 -0
- package/nitrogen/generated/android/c++/JHybridHybridPictureSelectorSpec.cpp +125 -0
- package/nitrogen/generated/android/c++/JHybridHybridPictureSelectorSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JMediaAsset.hpp +98 -0
- package/nitrogen/generated/android/c++/JMediaType.hpp +61 -0
- package/nitrogen/generated/android/c++/JPickerTheme.hpp +64 -0
- package/nitrogen/generated/android/c++/JPictureSelectorOptions.hpp +121 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/CompressOptions.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/CropOptions.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/HybridHybridPictureSelectorSpec.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/MediaAsset.kt +68 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/MediaType.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/NitroPictureSelectorOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/PickerTheme.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/margelo/pictureselector/PictureSelectorOptions.kt +65 -0
- package/nitrogen/generated/ios/NitroPictureSelector+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroPictureSelector-Swift-Cxx-Bridge.cpp +49 -0
- package/nitrogen/generated/ios/NitroPictureSelector-Swift-Cxx-Bridge.hpp +270 -0
- package/nitrogen/generated/ios/NitroPictureSelector-Swift-Cxx-Umbrella.hpp +65 -0
- package/nitrogen/generated/ios/c++/HybridHybridPictureSelectorSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridHybridPictureSelectorSpecSwift.hpp +110 -0
- package/nitrogen/generated/ios/swift/CompressOptions.swift +83 -0
- package/nitrogen/generated/ios/swift/CropOptions.swift +101 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_MediaAsset_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridHybridPictureSelectorSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridHybridPictureSelectorSpec_cxx.swift +176 -0
- package/nitrogen/generated/ios/swift/MediaAsset.swift +118 -0
- package/nitrogen/generated/ios/swift/MediaType.swift +44 -0
- package/nitrogen/generated/ios/swift/PickerTheme.swift +48 -0
- package/nitrogen/generated/ios/swift/PictureSelectorOptions.swift +182 -0
- package/nitrogen/generated/shared/c++/CompressOptions.hpp +95 -0
- package/nitrogen/generated/shared/c++/CropOptions.hpp +99 -0
- package/nitrogen/generated/shared/c++/HybridHybridPictureSelectorSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridHybridPictureSelectorSpec.hpp +69 -0
- package/nitrogen/generated/shared/c++/MediaAsset.hpp +124 -0
- package/nitrogen/generated/shared/c++/MediaType.hpp +80 -0
- package/nitrogen/generated/shared/c++/PickerTheme.hpp +84 -0
- package/nitrogen/generated/shared/c++/PictureSelectorOptions.hpp +132 -0
- package/package.json +76 -0
- package/src/PictureSelector.ts +72 -0
- package/src/index.ts +16 -0
- package/src/specs/PictureSelector.nitro.ts +121 -0
- package/src/types.ts +38 -0
- package/src/usePictureSelector.ts +102 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package com.margelo.pictureselector
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import androidx.fragment.app.Fragment
|
|
5
|
+
import com.luck.picture.lib.engine.CropFileEngine
|
|
6
|
+
import com.yalantis.ucrop.UCrop
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* uCrop integration for PictureSelector v3.
|
|
10
|
+
*
|
|
11
|
+
* PictureSelector calls [onStartCrop] when the user has selected an image
|
|
12
|
+
* and crop is configured. We forward to UCrop and it calls back into
|
|
13
|
+
* PictureSelector via the requestCode mechanism.
|
|
14
|
+
*/
|
|
15
|
+
class UCropEngine(
|
|
16
|
+
private val freeStyle: Boolean,
|
|
17
|
+
private val ratioX: Float,
|
|
18
|
+
private val ratioY: Float,
|
|
19
|
+
private val circular: Boolean,
|
|
20
|
+
private val quality: Int,
|
|
21
|
+
) : CropFileEngine {
|
|
22
|
+
|
|
23
|
+
override fun onStartCrop(
|
|
24
|
+
fragment: Fragment,
|
|
25
|
+
srcUri: Uri,
|
|
26
|
+
destinationUri: Uri,
|
|
27
|
+
dataSource: ArrayList<String>,
|
|
28
|
+
requestCode: Int,
|
|
29
|
+
) {
|
|
30
|
+
val options = UCrop.Options().apply {
|
|
31
|
+
setCompressionQuality(quality)
|
|
32
|
+
setHideBottomControls(false)
|
|
33
|
+
setFreeStyleCropEnabled(freeStyle)
|
|
34
|
+
if (circular) {
|
|
35
|
+
setCircleDimmedLayer(true)
|
|
36
|
+
setShowCropFrame(false)
|
|
37
|
+
setShowCropGrid(false)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
val uCrop = UCrop.of(srcUri, destinationUri)
|
|
42
|
+
.withOptions(options)
|
|
43
|
+
|
|
44
|
+
if (!freeStyle && !circular) {
|
|
45
|
+
uCrop.withAspectRatio(ratioX, ratioY)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
uCrop.start(fragment.requireContext(), fragment, requestCode)
|
|
50
|
+
} catch (e: Exception) {
|
|
51
|
+
throw PictureSelectorException(
|
|
52
|
+
"UNKNOWN",
|
|
53
|
+
"UCrop failed to start: ${e.message}"
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<paths>
|
|
3
|
+
<external-path name="external" path="." />
|
|
4
|
+
<external-cache-path name="external_cache" path="." />
|
|
5
|
+
<cache-path name="cache" path="." />
|
|
6
|
+
<files-path name="files" path="." />
|
|
7
|
+
<external-files-path name="external_files" path="." />
|
|
8
|
+
</paths>
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import HXPhotoPicker
|
|
4
|
+
import NitroModules
|
|
5
|
+
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
// HybridPictureSelector
|
|
8
|
+
//
|
|
9
|
+
// Main iOS implementation. Inherits from the nitrogen-generated
|
|
10
|
+
// HybridHybridPictureSelectorSpec_base and conforms to
|
|
11
|
+
// HybridHybridPictureSelectorSpec_protocol (together they form the
|
|
12
|
+
// HybridHybridPictureSelectorSpec typealias).
|
|
13
|
+
//
|
|
14
|
+
// Threading contract:
|
|
15
|
+
// - Nitro calls openPicker / openCamera on the JS thread.
|
|
16
|
+
// - All UIKit calls are dispatched to DispatchQueue.main.
|
|
17
|
+
// - 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
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
final class HybridPictureSelector: HybridHybridPictureSelectorSpec_base, HybridHybridPictureSelectorSpec_protocol {
|
|
32
|
+
|
|
33
|
+
// MARK: - Private state
|
|
34
|
+
|
|
35
|
+
/// Bundles the pending resolver together with the options so the delegate
|
|
36
|
+
/// can perform compression-aware result mapping.
|
|
37
|
+
private struct PendingSession {
|
|
38
|
+
let resolver: (Result<[MediaAsset], Error>) -> Void
|
|
39
|
+
let options: PictureSelectorOptions
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private var session: PendingSession?
|
|
43
|
+
|
|
44
|
+
/// Strong reference prevents picker deallocation before delegate fires.
|
|
45
|
+
private var activePicker: UIViewController?
|
|
46
|
+
|
|
47
|
+
// MARK: - openPicker
|
|
48
|
+
|
|
49
|
+
func openPicker(options: PictureSelectorOptions) -> Promise<[MediaAsset]> {
|
|
50
|
+
return Promise { [weak self] resolver in
|
|
51
|
+
guard let self = self else {
|
|
52
|
+
resolver.reject(PictureSelectorError.unknown("Instance deallocated"))
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
DispatchQueue.main.async {
|
|
57
|
+
guard let topVC = self.topViewController() else {
|
|
58
|
+
resolver.reject(PictureSelectorError.unknown(
|
|
59
|
+
"No active UIViewController. Ensure the picker is called from a mounted component."
|
|
60
|
+
))
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
self.session = PendingSession(
|
|
65
|
+
resolver: { result in
|
|
66
|
+
switch result {
|
|
67
|
+
case .success(let assets): resolver.resolve(assets)
|
|
68
|
+
case .failure(let err): resolver.reject(err)
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
options: options
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
let config = self.buildPickerConfig(from: options)
|
|
75
|
+
let picker = PhotoPickerController(picker: config)
|
|
76
|
+
picker.pickerDelegate = self
|
|
77
|
+
|
|
78
|
+
self.activePicker = picker
|
|
79
|
+
topVC.present(picker, animated: true)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// MARK: - openCamera
|
|
85
|
+
|
|
86
|
+
func openCamera(options: PictureSelectorOptions) -> Promise<[MediaAsset]> {
|
|
87
|
+
return Promise { [weak self] resolver in
|
|
88
|
+
guard let self = self else {
|
|
89
|
+
resolver.reject(PictureSelectorError.unknown("Instance deallocated"))
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
DispatchQueue.main.async {
|
|
94
|
+
guard let topVC = self.topViewController() else {
|
|
95
|
+
resolver.reject(PictureSelectorError.unknown("No active UIViewController."))
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var cameraConfig = CameraConfiguration()
|
|
100
|
+
switch options.mediaType {
|
|
101
|
+
case "video": cameraConfig.mediaType = .video
|
|
102
|
+
default: cameraConfig.mediaType = .photo
|
|
103
|
+
}
|
|
104
|
+
if let maxDur = options.maxVideoDuration {
|
|
105
|
+
cameraConfig.videoMaximumDuration = maxDur
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// API REQUIRES VERIFICATION:
|
|
109
|
+
// Photo.camera(_:fromViewController:completion:cancel:) method signature.
|
|
110
|
+
// If this overload doesn't exist, use CameraController directly:
|
|
111
|
+
// let cam = CameraController(config: cameraConfig)
|
|
112
|
+
// cam.onCompletion = { ... }
|
|
113
|
+
// topVC.present(cam, animated: true)
|
|
114
|
+
Photo.camera(
|
|
115
|
+
cameraConfig,
|
|
116
|
+
fromViewController: topVC
|
|
117
|
+
) { [weak self] result, _ in
|
|
118
|
+
guard let self = self else { return }
|
|
119
|
+
guard let photoAsset = result?.photoAsset else {
|
|
120
|
+
resolver.resolve([])
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
Task {
|
|
124
|
+
do {
|
|
125
|
+
let asset = try await self.mapAsset(
|
|
126
|
+
photoAsset,
|
|
127
|
+
compress: options.compress,
|
|
128
|
+
isOriginal: false
|
|
129
|
+
)
|
|
130
|
+
resolver.resolve([asset])
|
|
131
|
+
} catch {
|
|
132
|
+
resolver.reject(error)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} cancel: { _ in
|
|
136
|
+
resolver.reject(PictureSelectorError.cancelled)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// MARK: - Config builder
|
|
143
|
+
|
|
144
|
+
private func buildPickerConfig(from options: PictureSelectorOptions) -> PickerConfiguration {
|
|
145
|
+
var config = PickerConfiguration()
|
|
146
|
+
|
|
147
|
+
// Media type
|
|
148
|
+
switch options.mediaType {
|
|
149
|
+
case "video":
|
|
150
|
+
config.selectOptions = [.video]
|
|
151
|
+
case "all":
|
|
152
|
+
config.selectOptions = [.photo, .video]
|
|
153
|
+
default:
|
|
154
|
+
config.selectOptions = [.photo]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Selection limit
|
|
158
|
+
config.maximumSelectedCount = Int(options.maxCount ?? 1)
|
|
159
|
+
|
|
160
|
+
// In-picker camera button
|
|
161
|
+
config.allowCustomCamera = options.enableCamera ?? true
|
|
162
|
+
|
|
163
|
+
// Video duration limits
|
|
164
|
+
if let maxDur = options.maxVideoDuration {
|
|
165
|
+
config.maximumSelectedVideoDuration = maxDur
|
|
166
|
+
}
|
|
167
|
+
if let minDur = options.minVideoDuration {
|
|
168
|
+
config.minimumSelectedVideoDuration = minDur
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Editor / crop (only when maxCount == 1)
|
|
172
|
+
let maxCount = Int(options.maxCount ?? 1)
|
|
173
|
+
if let crop = options.crop, crop.enabled, maxCount == 1 {
|
|
174
|
+
config.editorOptions = [.photo]
|
|
175
|
+
var editorConfig = EditorConfiguration()
|
|
176
|
+
|
|
177
|
+
var cropSizeConfig = EditorConfiguration.Photo.CropSize()
|
|
178
|
+
if crop.circular == true {
|
|
179
|
+
// API REQUIRES VERIFICATION: isRoundCrop property name in v5.0.5
|
|
180
|
+
cropSizeConfig.isRoundCrop = true
|
|
181
|
+
} else if crop.freeStyle == true {
|
|
182
|
+
cropSizeConfig.aspectRatios = [] // empty array = free style
|
|
183
|
+
} else {
|
|
184
|
+
let x = crop.ratioX ?? 1.0
|
|
185
|
+
let y = crop.ratioY ?? 1.0
|
|
186
|
+
cropSizeConfig.aspectRatios = [
|
|
187
|
+
.init(title: "", ratio: .init(width: x, height: y))
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
editorConfig.photo.cropSize = cropSizeConfig
|
|
191
|
+
config.editor = editorConfig
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Theme color
|
|
195
|
+
if let hex = options.themeColor, let color = UIColor(hex: hex) {
|
|
196
|
+
config.themeColor = color
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return config
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// MARK: - Result mapping
|
|
203
|
+
|
|
204
|
+
private func mapResults(
|
|
205
|
+
_ result: PickerResult,
|
|
206
|
+
compress: CompressOptions?
|
|
207
|
+
) async throws -> [MediaAsset] {
|
|
208
|
+
var mapped: [MediaAsset] = []
|
|
209
|
+
for photoAsset in result.photoAssets {
|
|
210
|
+
let asset = try await mapAsset(
|
|
211
|
+
photoAsset,
|
|
212
|
+
compress: compress,
|
|
213
|
+
isOriginal: result.isOriginal
|
|
214
|
+
)
|
|
215
|
+
mapped.append(asset)
|
|
216
|
+
}
|
|
217
|
+
return mapped
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private func mapAsset(
|
|
221
|
+
_ photoAsset: PhotoAsset,
|
|
222
|
+
compress: CompressOptions?,
|
|
223
|
+
isOriginal: Bool
|
|
224
|
+
) async throws -> MediaAsset {
|
|
225
|
+
// Obtain file URL via callback, bridged to async/await.
|
|
226
|
+
// API REQUIRES VERIFICATION: getURL(compression:result:) availability in v5.0.5.
|
|
227
|
+
let urlResult: AssetURLResult = try await withCheckedThrowingContinuation { cont in
|
|
228
|
+
photoAsset.getURL(
|
|
229
|
+
compression: buildCompression(from: compress)
|
|
230
|
+
) { result in
|
|
231
|
+
switch result {
|
|
232
|
+
case .success(let r): cont.resume(returning: r)
|
|
233
|
+
case .failure(let e): cont.resume(throwing: e)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Determine if the user applied edits
|
|
239
|
+
let wasEdited = photoAsset.editedResult != nil
|
|
240
|
+
let finalUri = urlResult.url.absoluteString
|
|
241
|
+
let editedUri: String? = wasEdited ? finalUri : nil
|
|
242
|
+
|
|
243
|
+
// Image / video dimensions
|
|
244
|
+
// API REQUIRES VERIFICATION: imageSize property name in v5.0.5
|
|
245
|
+
let size: CGSize = photoAsset.imageSize
|
|
246
|
+
|
|
247
|
+
// Duration: HXPhotoPicker returns seconds; bridge spec expects ms.
|
|
248
|
+
// API REQUIRES VERIFICATION: videoDuration property name and unit.
|
|
249
|
+
let durationMs: Double = (photoAsset.videoDuration ?? 0) * 1_000
|
|
250
|
+
|
|
251
|
+
// File size
|
|
252
|
+
// API REQUIRES VERIFICATION: AssetURLResult.fileSize field name.
|
|
253
|
+
let fileSize: Double = Double(urlResult.fileSize ?? 0)
|
|
254
|
+
|
|
255
|
+
let typeStr: String = (photoAsset.mediaType == .video) ? "video" : "image"
|
|
256
|
+
|
|
257
|
+
return MediaAsset(
|
|
258
|
+
uri: finalUri,
|
|
259
|
+
type: typeStr,
|
|
260
|
+
mimeType: mimeType(for: urlResult.url),
|
|
261
|
+
width: Double(size.width),
|
|
262
|
+
height: Double(size.height),
|
|
263
|
+
duration: durationMs,
|
|
264
|
+
fileName: urlResult.url.lastPathComponent,
|
|
265
|
+
fileSize: fileSize,
|
|
266
|
+
editedUri: editedUri,
|
|
267
|
+
isOriginal: isOriginal,
|
|
268
|
+
bucketName: nil
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// MARK: - Compression helper
|
|
273
|
+
|
|
274
|
+
/// API REQUIRES VERIFICATION: PhotoAsset.Compression type name and init params in v5.0.5.
|
|
275
|
+
private func buildCompression(from options: CompressOptions?) -> PhotoAsset.Compression? {
|
|
276
|
+
guard let opts = options, opts.enabled else { return nil }
|
|
277
|
+
return PhotoAsset.Compression(
|
|
278
|
+
imageCompressionQuality: opts.quality ?? 0.8
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// MARK: - Top view controller
|
|
283
|
+
|
|
284
|
+
private func topViewController() -> UIViewController? {
|
|
285
|
+
let windowScene = UIApplication.shared.connectedScenes
|
|
286
|
+
.filter { $0.activationState == .foregroundActive }
|
|
287
|
+
.compactMap { $0 as? UIWindowScene }
|
|
288
|
+
.first
|
|
289
|
+
|
|
290
|
+
guard let window = windowScene?.windows.first(where: { $0.isKeyWindow }) else {
|
|
291
|
+
return nil
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
var top: UIViewController? = window.rootViewController
|
|
295
|
+
while let presented = top?.presentedViewController {
|
|
296
|
+
top = presented
|
|
297
|
+
}
|
|
298
|
+
return top
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// MARK: - MIME type helper
|
|
302
|
+
|
|
303
|
+
private func mimeType(for url: URL) -> String {
|
|
304
|
+
switch url.pathExtension.lowercased() {
|
|
305
|
+
case "jpg", "jpeg": return "image/jpeg"
|
|
306
|
+
case "png": return "image/png"
|
|
307
|
+
case "gif": return "image/gif"
|
|
308
|
+
case "heic": return "image/heic"
|
|
309
|
+
case "heif": return "image/heif"
|
|
310
|
+
case "webp": return "image/webp"
|
|
311
|
+
case "mp4": return "video/mp4"
|
|
312
|
+
case "mov": return "video/quicktime"
|
|
313
|
+
case "m4v": return "video/x-m4v"
|
|
314
|
+
default: return "application/octet-stream"
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// MARK: - PhotoPickerControllerDelegate
|
|
320
|
+
|
|
321
|
+
extension HybridPictureSelector: PhotoPickerControllerDelegate {
|
|
322
|
+
|
|
323
|
+
func pickerController(
|
|
324
|
+
_ pickerController: PhotoPickerController,
|
|
325
|
+
didFinishSelection result: PickerResult
|
|
326
|
+
) {
|
|
327
|
+
// Capture and clear session atomically before dismiss completes
|
|
328
|
+
let captured = session
|
|
329
|
+
session = nil
|
|
330
|
+
activePicker = nil
|
|
331
|
+
|
|
332
|
+
pickerController.dismiss(animated: true) { [weak self] in
|
|
333
|
+
guard let self = self, let s = captured else { return }
|
|
334
|
+
Task {
|
|
335
|
+
do {
|
|
336
|
+
let assets = try await self.mapResults(result, compress: s.options.compress)
|
|
337
|
+
s.resolver(.success(assets))
|
|
338
|
+
} catch {
|
|
339
|
+
s.resolver(.failure(error))
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
func pickerController(didCancel pickerController: PhotoPickerController) {
|
|
346
|
+
let captured = session
|
|
347
|
+
session = nil
|
|
348
|
+
activePicker = nil
|
|
349
|
+
|
|
350
|
+
pickerController.dismiss(animated: true) {
|
|
351
|
+
captured?.resolver(.failure(PictureSelectorError.cancelled))
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// MARK: - Error type
|
|
357
|
+
|
|
358
|
+
enum PictureSelectorError: Error, LocalizedError {
|
|
359
|
+
case cancelled
|
|
360
|
+
case permissionDenied
|
|
361
|
+
case unknown(String)
|
|
362
|
+
|
|
363
|
+
var errorDescription: String? {
|
|
364
|
+
switch self {
|
|
365
|
+
case .cancelled: return "CANCELLED"
|
|
366
|
+
case .permissionDenied: return "PERMISSION_DENIED"
|
|
367
|
+
case .unknown(let msg): return "UNKNOWN: \(msg)"
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// MARK: - UIColor hex initialiser
|
|
373
|
+
|
|
374
|
+
extension UIColor {
|
|
375
|
+
convenience init?(hex: String) {
|
|
376
|
+
var s = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
377
|
+
if s.hasPrefix("#") { s = String(s.dropFirst()) }
|
|
378
|
+
guard s.count == 6, let rgb = UInt64(s, radix: 16) else { return nil }
|
|
379
|
+
self.init(
|
|
380
|
+
red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0,
|
|
381
|
+
green: CGFloat((rgb & 0x00FF00) >> 8) / 255.0,
|
|
382
|
+
blue: CGFloat( rgb & 0x0000FF ) / 255.0,
|
|
383
|
+
alpha: 1.0
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
package = JSON.parse(File.read(File.join(__dir__, "..", "package.json")))
|
|
3
|
+
|
|
4
|
+
Pod::Spec.new do |s|
|
|
5
|
+
s.name = "NitroPictureSelector"
|
|
6
|
+
s.version = package["version"]
|
|
7
|
+
s.summary = package["description"]
|
|
8
|
+
s.homepage = package["repository"]["url"]
|
|
9
|
+
s.license = { :type => "MIT" }
|
|
10
|
+
s.author = "react-native-picture-selector contributors"
|
|
11
|
+
|
|
12
|
+
s.platforms = { :ios => "13.0" }
|
|
13
|
+
s.swift_version = "5.9"
|
|
14
|
+
s.requires_arc = true
|
|
15
|
+
|
|
16
|
+
# ── Sources ─────────────────────────────────────────────────────────────
|
|
17
|
+
# Include both our hand-written Swift and the nitrogen-generated bridge files.
|
|
18
|
+
s.source_files = [
|
|
19
|
+
"ios/**/*.{h,m,mm,swift}",
|
|
20
|
+
"../nitrogen/generated/ios/**/*.{h,m,mm,swift,hpp,cpp}",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# ── Dependencies ─────────────────────────────────────────────────────────
|
|
24
|
+
s.dependency "React-Core"
|
|
25
|
+
s.dependency "react-native-nitro-modules"
|
|
26
|
+
|
|
27
|
+
# HXPhotoPicker v5 — default subspec includes Picker + Editor + Camera
|
|
28
|
+
s.dependency "HXPhotoPicker", "~> 5.0.5"
|
|
29
|
+
|
|
30
|
+
# ── Compiler flags ───────────────────────────────────────────────────────
|
|
31
|
+
# Enable HXPhotoPicker conditional compilation flags
|
|
32
|
+
# Enable Swift/C++ interop required by Nitro for zero-overhead calls
|
|
33
|
+
s.pod_target_xcconfig = {
|
|
34
|
+
"SWIFT_ACTIVE_COMPILATION_CONDITIONS" =>
|
|
35
|
+
"HXPICKER_ENABLE_CORE HXPICKER_ENABLE_PICKER HXPICKER_ENABLE_EDITOR HXPICKER_ENABLE_CAMERA",
|
|
36
|
+
"OTHER_SWIFT_FLAGS" => "-enable-experimental-cxx-interop",
|
|
37
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
|
|
38
|
+
}
|
|
39
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.PictureSelector = void 0;
|
|
7
|
+
var _reactNativeNitroModules = require("react-native-nitro-modules");
|
|
8
|
+
var _types = require("./types");
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
// Lazy singleton — created once and reused across calls
|
|
11
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
let _native = null;
|
|
14
|
+
function getNative() {
|
|
15
|
+
if (_native == null) {
|
|
16
|
+
_native = _reactNativeNitroModules.NitroModules.createHybridObject('PictureSelector');
|
|
17
|
+
}
|
|
18
|
+
return _native;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
// Default options
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const defaultOptions = {
|
|
26
|
+
mediaType: _types.MediaType.IMAGE,
|
|
27
|
+
maxCount: 1,
|
|
28
|
+
enableCamera: true
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Static API
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const PictureSelector = exports.PictureSelector = {
|
|
36
|
+
/**
|
|
37
|
+
* Open the gallery picker.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const assets = await PictureSelector.openPicker({ maxCount: 9 })
|
|
41
|
+
*
|
|
42
|
+
* @throws PickerError with code CANCELLED when the user dismisses
|
|
43
|
+
* @throws PickerError with code PERMISSION_DENIED on permission failure
|
|
44
|
+
*/
|
|
45
|
+
async openPicker(options = {}) {
|
|
46
|
+
try {
|
|
47
|
+
return await getNative().openPicker({
|
|
48
|
+
...defaultOptions,
|
|
49
|
+
...options
|
|
50
|
+
});
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw (0, _types.toPickerError)(err);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
/**
|
|
56
|
+
* Open the camera for capture.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* const [asset] = await PictureSelector.openCamera({ mediaType: MediaType.VIDEO })
|
|
60
|
+
*
|
|
61
|
+
* @throws PickerError with code CANCELLED when the user dismisses
|
|
62
|
+
*/
|
|
63
|
+
async openCamera(options = {}) {
|
|
64
|
+
try {
|
|
65
|
+
return await getNative().openCamera({
|
|
66
|
+
...defaultOptions,
|
|
67
|
+
...options
|
|
68
|
+
});
|
|
69
|
+
} catch (err) {
|
|
70
|
+
throw (0, _types.toPickerError)(err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=PictureSelector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["_reactNativeNitroModules","require","_types","_native","getNative","NitroModules","createHybridObject","defaultOptions","mediaType","MediaType","IMAGE","maxCount","enableCamera","PictureSelector","exports","openPicker","options","err","toPickerError","openCamera"],"sourceRoot":"..\\..\\src","sources":["PictureSelector.ts"],"mappings":";;;;;;AAAA,IAAAA,wBAAA,GAAAC,OAAA;AAGA,IAAAC,MAAA,GAAAD,OAAA;AAEA;AACA;AACA;;AAEA,IAAIE,OAAqC,GAAG,IAAI;AAEhD,SAASC,SAASA,CAAA,EAA0B;EAC1C,IAAID,OAAO,IAAI,IAAI,EAAE;IACnBA,OAAO,GAAGE,qCAAY,CAACC,kBAAkB,CACvC,iBACF,CAAC;EACH;EACA,OAAOH,OAAO;AAChB;;AAEA;AACA;AACA;;AAEA,MAAMI,cAAsC,GAAG;EAC7CC,SAAS,EAAEC,gBAAS,CAACC,KAAK;EAC1BC,QAAQ,EAAE,CAAC;EACXC,YAAY,EAAE;AAChB,CAAC;;AAED;AACA;AACA;;AAEO,MAAMC,eAAe,GAAAC,OAAA,CAAAD,eAAA,GAAG;EAC7B;AACF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACE,MAAME,UAAUA,CACdC,OAA+B,GAAG,CAAC,CAAC,EACb;IACvB,IAAI;MACF,OAAO,MAAMZ,SAAS,CAAC,CAAC,CAACW,UAAU,CAAC;QAAE,GAAGR,cAAc;QAAE,GAAGS;MAAQ,CAAC,CAAC;IACxE,CAAC,CAAC,OAAOC,GAAG,EAAE;MACZ,MAAM,IAAAC,oBAAa,EAACD,GAAG,CAAC;IAC1B;EACF,CAAC;EAED;AACF;AACA;AACA;AACA;AACA;AACA;AACA;EACE,MAAME,UAAUA,CACdH,OAA+B,GAAG,CAAC,CAAC,EACb;IACvB,IAAI;MACF,OAAO,MAAMZ,SAAS,CAAC,CAAC,CAACe,UAAU,CAAC;QAAE,GAAGZ,cAAc;QAAE,GAAGS;MAAQ,CAAC,CAAC;IACxE,CAAC,CAAC,OAAOC,GAAG,EAAE;MACZ,MAAM,IAAAC,oBAAa,EAACD,GAAG,CAAC;IAC1B;EACF;AACF,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "MediaType", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _types.MediaType;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(exports, "PickerTheme", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () {
|
|
15
|
+
return _types.PickerTheme;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(exports, "PictureSelector", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () {
|
|
21
|
+
return _PictureSelector.PictureSelector;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(exports, "toPickerError", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
get: function () {
|
|
27
|
+
return _types.toPickerError;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(exports, "usePictureSelector", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
get: function () {
|
|
33
|
+
return _usePictureSelector.usePictureSelector;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
var _PictureSelector = require("./PictureSelector");
|
|
37
|
+
var _usePictureSelector = require("./usePictureSelector");
|
|
38
|
+
var _types = require("./types");
|
|
39
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["_PictureSelector","require","_usePictureSelector","_types"],"sourceRoot":"..\\..\\src","sources":["index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,gBAAA,GAAAC,OAAA;AACA,IAAAC,mBAAA,GAAAD,OAAA;AAcA,IAAAE,MAAA,GAAAF,OAAA","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.PickerTheme = exports.MediaType = void 0;
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
// Enums
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
let MediaType = exports.MediaType = /*#__PURE__*/function (MediaType) {
|
|
11
|
+
MediaType["IMAGE"] = "image";
|
|
12
|
+
MediaType["VIDEO"] = "video";
|
|
13
|
+
MediaType["ALL"] = "all";
|
|
14
|
+
return MediaType;
|
|
15
|
+
}({});
|
|
16
|
+
let PickerTheme = exports.PickerTheme = /*#__PURE__*/function (PickerTheme) {
|
|
17
|
+
PickerTheme["DEFAULT"] = "default";
|
|
18
|
+
PickerTheme["WECHAT"] = "wechat";
|
|
19
|
+
PickerTheme["WHITE"] = "white";
|
|
20
|
+
PickerTheme["DARK"] = "dark";
|
|
21
|
+
return PickerTheme;
|
|
22
|
+
}({}); // ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
// Config sub-structures
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
// Picker options
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
// Result structure
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// HybridObject — the main bridge
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
//# sourceMappingURL=PictureSelector.nitro.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["MediaType","exports","PickerTheme"],"sourceRoot":"..\\..\\..\\src","sources":["specs/PictureSelector.nitro.ts"],"mappings":";;;;;;AAEA;AACA;AACA;AAAA,IAEYA,SAAS,GAAAC,OAAA,CAAAD,SAAA,0BAATA,SAAS;EAATA,SAAS;EAATA,SAAS;EAATA,SAAS;EAAA,OAATA,SAAS;AAAA;AAAA,IAMTE,WAAW,GAAAD,OAAA,CAAAC,WAAA,0BAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAXA,WAAW;EAAA,OAAXA,WAAW;AAAA,OAOvB;AACA;AACA;AA0BA;AACA;AACA;AAyBA;AACA;AACA;AA2BA;AACA;AACA","ignoreList":[]}
|