react-native-attachment-kit 1.0.8 → 1.0.10
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/CHANGELOG.md +10 -0
- package/README.md +40 -2
- package/android/CMakeLists.txt +3 -0
- package/android/build.gradle +11 -2
- package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
- package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +48 -2
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +48 -2
- package/ios/RNAttachmentKit.swift +166 -15
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +738 -426
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +12 -12
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +738 -426
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +738 -426
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +12 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +22 -22
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/types.ts +34 -1
|
@@ -57,6 +57,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
57
57
|
/// 동영상 변환 진행 HUD 를 띄울지 — video.showProgressHUD, 없으면 asyncProcessing 이 아닐 때만
|
|
58
58
|
/// (앱이 onProcessingStart / onVideoProgress / onImageProcessed 로 말풍선에 진행 상황을 그리는 구조면 HUD 없이)
|
|
59
59
|
private var galleryShowsVideoHUD = true
|
|
60
|
+
/// 사진 썸네일 (GalleryOptions.thumbnail). nil 이면 만들지 않음 (기존과 같음)
|
|
61
|
+
private var galleryThumbnail: PhotoThumbnailOptions?
|
|
60
62
|
|
|
61
63
|
// MARK: - Module Setup
|
|
62
64
|
|
|
@@ -240,6 +242,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
240
242
|
func openMapViewer(_ options: NSDictionary,
|
|
241
243
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
242
244
|
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
245
|
+
guard LicenseGate.isLicensed else {
|
|
246
|
+
reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
|
|
247
|
+
}
|
|
243
248
|
guard let lat = RNAttachmentKit.cgFloat(options["latitude"]),
|
|
244
249
|
let lng = RNAttachmentKit.cgFloat(options["longitude"]) else {
|
|
245
250
|
reject("ERROR", "latitude and longitude are required", nil)
|
|
@@ -308,6 +313,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
308
313
|
func renderLocationThumbnail(_ options: NSDictionary,
|
|
309
314
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
310
315
|
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
316
|
+
guard LicenseGate.isLicensed else {
|
|
317
|
+
reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
|
|
318
|
+
}
|
|
311
319
|
guard let lat = RNAttachmentKit.cgFloat(options["latitude"]),
|
|
312
320
|
let lng = RNAttachmentKit.cgFloat(options["longitude"]),
|
|
313
321
|
let w = RNAttachmentKit.cgFloat(options["width"]), w > 0 else {
|
|
@@ -366,6 +374,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
366
374
|
func reverseGeocode(_ options: NSDictionary,
|
|
367
375
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
368
376
|
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
377
|
+
guard LicenseGate.isLicensed else {
|
|
378
|
+
reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
|
|
379
|
+
}
|
|
369
380
|
let lat = (options["latitude"] as? NSNumber)?.doubleValue ?? .nan
|
|
370
381
|
let lng = (options["longitude"] as? NSNumber)?.doubleValue ?? .nan
|
|
371
382
|
let lang = (options["languageCode"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? "en"
|
|
@@ -383,6 +394,10 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
383
394
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
384
395
|
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
385
396
|
let dict = config as? [String: Any] ?? [:]
|
|
397
|
+
// 라이선스 키가 없어도 설정은 받아 둠 (AI 버튼은 보이고, 누르면 "라이선스 필요" 안내)
|
|
398
|
+
if !LicenseGate.isLicensed, (dict["enabled"] as? Bool) == true {
|
|
399
|
+
NSLog("[RNAttachmentKit] AI needs a license key — the AI buttons show a license notice until init() gets a valid key")
|
|
400
|
+
}
|
|
386
401
|
DispatchQueue.main.async {
|
|
387
402
|
AIBilling.apply(dict)
|
|
388
403
|
AIBilling.onUsage = { [weak self] body in self?.sendEvent(withName: "onAIUsage", body: body) }
|
|
@@ -421,6 +436,10 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
421
436
|
let cacheFiles: [String]? = (options["cacheFiles"] as? [Any])?.map { ($0 as? String) ?? "" }
|
|
422
437
|
// videos: images 와 같은 순서. 항목이 있으면 그 페이지는 동영상 (images[i] 는 포스터)
|
|
423
438
|
let viewerVideos: [ViewerVideoItem?] = (options["videos"] as? [Any])?.map { ViewerVideoItem(dictionary: $0) } ?? []
|
|
439
|
+
// 동영상 페이지는 라이선스 키가 있어야
|
|
440
|
+
if !LicenseGate.isLicensed, viewerVideos.contains(where: { $0 != nil }) {
|
|
441
|
+
reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
|
|
442
|
+
}
|
|
424
443
|
let themeColorHex = options["themeColor"] as? String
|
|
425
444
|
let languageCode = options["languageCode"] as? String ?? "en"
|
|
426
445
|
let viewerTitle = options["title"] as? String
|
|
@@ -679,9 +698,10 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
679
698
|
var parsedMaxHeight: Int? = nil
|
|
680
699
|
if let maxW = options["maxWidth"] as? Int { parsedMaxWidth = maxW }
|
|
681
700
|
if let maxH = options["maxHeight"] as? Int { parsedMaxHeight = maxH }
|
|
701
|
+
let parsedThumbnail = PhotoThumbnailOptions(options["thumbnail"])
|
|
682
702
|
|
|
683
703
|
DispatchQueue.main.async {
|
|
684
|
-
guard
|
|
704
|
+
guard self.getRootViewController() != nil else {
|
|
685
705
|
reject("ERROR", "Unable to get root view controller", nil)
|
|
686
706
|
return
|
|
687
707
|
}
|
|
@@ -716,7 +736,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
716
736
|
|
|
717
737
|
editorVC.onConfirm = { [weak self, weak editorVC] (editedAsset, editedImage, filterState, caption) in
|
|
718
738
|
editorVC?.dismiss(animated: true) {
|
|
719
|
-
self?.processEditorResult(asset: editedAsset, croppedImage: editedImage, filterState: filterState, caption: caption, galleryVC: nil, targetMaxWidth: parsedMaxWidth, targetMaxHeight: parsedMaxHeight, resolve: { resultArr in
|
|
739
|
+
self?.processEditorResult(asset: editedAsset, croppedImage: editedImage, filterState: filterState, caption: caption, galleryVC: nil, targetMaxWidth: parsedMaxWidth, targetMaxHeight: parsedMaxHeight, thumbnail: parsedThumbnail, resolve: { resultArr in
|
|
720
740
|
if let arr = resultArr as? [[String: Any]], let first = arr.first {
|
|
721
741
|
resolve(first)
|
|
722
742
|
} else {
|
|
@@ -732,11 +752,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
732
752
|
}
|
|
733
753
|
}
|
|
734
754
|
|
|
735
|
-
|
|
736
|
-
while let presented = topController.presentedViewController {
|
|
737
|
-
topController = presented
|
|
738
|
-
}
|
|
739
|
-
topController.present(editorVC, animated: true)
|
|
755
|
+
self.presentOnTop(editorVC, reject: reject)
|
|
740
756
|
|
|
741
757
|
} else {
|
|
742
758
|
guard let url = URL(string: uri) ?? URL(string: "file://" + uri.replacingOccurrences(of: "file://", with: "")) else {
|
|
@@ -760,7 +776,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
760
776
|
|
|
761
777
|
editorVC.onConfirm = { [weak self, weak editorVC] (editedAsset, editedImage, filterState, caption) in
|
|
762
778
|
editorVC?.dismiss(animated: true) {
|
|
763
|
-
self?.processEditorResult(asset: editedAsset, croppedImage: editedImage, filterState: filterState, caption: caption, galleryVC: nil, targetMaxWidth: parsedMaxWidth, targetMaxHeight: parsedMaxHeight, resolve: { resultArr in
|
|
779
|
+
self?.processEditorResult(asset: editedAsset, croppedImage: editedImage, filterState: filterState, caption: caption, galleryVC: nil, targetMaxWidth: parsedMaxWidth, targetMaxHeight: parsedMaxHeight, thumbnail: parsedThumbnail, resolve: { resultArr in
|
|
764
780
|
var resultDict = (resultArr as? [[String: Any]])?.first ?? (resultArr as? [String: Any]) ?? [:]
|
|
765
781
|
// For remote URLs, we preserve originalUri if editedFileUri isn't provided
|
|
766
782
|
let originalRemoteUrl = (options["editedFileUri"] as? String) ?? uri
|
|
@@ -776,11 +792,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
776
792
|
}
|
|
777
793
|
}
|
|
778
794
|
|
|
779
|
-
|
|
780
|
-
while let presented = topController.presentedViewController {
|
|
781
|
-
topController = presented
|
|
782
|
-
}
|
|
783
|
-
topController.present(editorVC, animated: true)
|
|
795
|
+
self.presentOnTop(editorVC, reject: reject)
|
|
784
796
|
}
|
|
785
797
|
} else {
|
|
786
798
|
DispatchQueue.main.async {
|
|
@@ -868,6 +880,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
868
880
|
// maxWidth, maxHeight 옵션 (미설정 또는 0이면 원본 크기 유지)
|
|
869
881
|
galleryVC.maxWidth = (options["maxWidth"] as? Int) ?? 0
|
|
870
882
|
galleryVC.maxHeight = (options["maxHeight"] as? Int) ?? 0
|
|
883
|
+
// 사진 썸네일 (동영상 포스터 규칙은 그대로 — video.posterFormat, 긴 변 512)
|
|
884
|
+
self.galleryThumbnail = PhotoThumbnailOptions(options["thumbnail"])
|
|
871
885
|
|
|
872
886
|
// outputFormat: "webp" | "jpg" | "png" (기본값: "webp")
|
|
873
887
|
let fmt = (options["outputFormat"] as? String)?.lowercased() ?? "webp"
|
|
@@ -973,6 +987,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
973
987
|
// 동영상: mediaType "photo"(기본) | "video" | "all" + video 옵션 (지시서 3-1)
|
|
974
988
|
let mediaTypeOption = (options["mediaType"] as? String)?.lowercased() ?? "photo"
|
|
975
989
|
galleryVC.mediaType = ["photo", "video", "all"].contains(mediaTypeOption) ? mediaTypeOption : "photo"
|
|
990
|
+
// 동영상은 라이선스 키가 있어야 — 없으면 갤러리가 사진만 보여 줌 (GalleryBaseViewController.viewDidLoad)
|
|
976
991
|
let videoOptions = VideoExportOptions(dictionary: options["video"] as? [String: Any])
|
|
977
992
|
let asyncProcessing = options["asyncProcessing"] as? Bool ?? false
|
|
978
993
|
self.galleryShowsVideoHUD = ((options["video"] as? [String: Any])?["showProgressHUD"] as? Bool) ?? !asyncProcessing
|
|
@@ -1004,6 +1019,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1004
1019
|
let galleryFormat = galleryVC.outputFormat // "webp" | "jpg" | "png"
|
|
1005
1020
|
let galleryAccent = galleryVC.parsedThemeColor
|
|
1006
1021
|
let galleryLanguage = galleryVC.languageCode
|
|
1022
|
+
let galleryThumb = self.galleryThumbnail
|
|
1007
1023
|
galleryVC.onImagesSelected = { [weak self, weak galleryVC] selectedImages in
|
|
1008
1024
|
guard let self = self else { resolve([]); return }
|
|
1009
1025
|
|
|
@@ -1176,6 +1192,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1176
1192
|
result["fileExtension"] = meta.fileExtension
|
|
1177
1193
|
result["fileSize"] = meta.fileSize
|
|
1178
1194
|
}
|
|
1195
|
+
// 썸네일: 같은 비트맵(방향·크롭·그리기 반영)에서
|
|
1196
|
+
result.merge(self.makePhotoThumbnail(targetImage, options: galleryThumb, index: index,
|
|
1197
|
+
timestamp: timestamp, tempDir: tempDir)) { $1 }
|
|
1179
1198
|
|
|
1180
1199
|
resultsLock.lock()
|
|
1181
1200
|
results.append(result)
|
|
@@ -1227,6 +1246,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1227
1246
|
result["fileExtension"] = meta.fileExtension
|
|
1228
1247
|
result["fileSize"] = meta.fileSize
|
|
1229
1248
|
}
|
|
1249
|
+
// 썸네일: 같은 비트맵(방향·크롭·그리기 반영)에서
|
|
1250
|
+
result.merge(self.makePhotoThumbnail(targetImage, options: galleryThumb, index: index,
|
|
1251
|
+
timestamp: timestamp, tempDir: tempDir)) { $1 }
|
|
1230
1252
|
|
|
1231
1253
|
resultsLock.lock()
|
|
1232
1254
|
results.append(result)
|
|
@@ -1621,6 +1643,66 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1621
1643
|
}
|
|
1622
1644
|
}
|
|
1623
1645
|
|
|
1646
|
+
// MARK: - Photo thumbnail (GalleryOptions.thumbnail / EditorOptions.thumbnail)
|
|
1647
|
+
|
|
1648
|
+
/// 결과 크기와 원본에서 잘라 쓸 영역 (픽셀). Android PhotoThumbnail.layout 과 같은 계산
|
|
1649
|
+
/// - fit: ratio = min(maxW/w, maxH/h, 1) → round(w·ratio) × round(h·ratio), 자르지 않음
|
|
1650
|
+
/// - cover: 상자 비율로 가운데를 자른 뒤 maxW×maxH 로 줄임. 원본이 상자보다 작으면 확대하지 않고 자른 크기 그대로
|
|
1651
|
+
static func photoThumbnailLayout(width w: Int, height h: Int, options o: PhotoThumbnailOptions) -> (crop: CGRect, size: CGSize) {
|
|
1652
|
+
let dw = Double(w), dh = Double(h)
|
|
1653
|
+
if !o.cover {
|
|
1654
|
+
let r = min(Double(o.maxWidth) / dw, Double(o.maxHeight) / dh, 1)
|
|
1655
|
+
return (CGRect(x: 0, y: 0, width: w, height: h),
|
|
1656
|
+
CGSize(width: max(1, (dw * r).rounded()), height: max(1, (dh * r).rounded())))
|
|
1657
|
+
}
|
|
1658
|
+
let aspect = Double(o.maxWidth) / Double(o.maxHeight)
|
|
1659
|
+
let cropW: Double, cropH: Double
|
|
1660
|
+
if dw / dh > aspect { cropH = dh; cropW = dh * aspect } else { cropW = dw; cropH = dw / aspect }
|
|
1661
|
+
let left = ((dw - cropW) / 2).rounded(), top = ((dh - cropH) / 2).rounded()
|
|
1662
|
+
let crop = CGRect(x: left, y: top, width: min(dw - left, cropW.rounded()), height: min(dh - top, cropH.rounded()))
|
|
1663
|
+
let size = cropW > Double(o.maxWidth)
|
|
1664
|
+
? CGSize(width: o.maxWidth, height: o.maxHeight)
|
|
1665
|
+
: CGSize(width: max(1, cropW.rounded()), height: max(1, cropH.rounded()))
|
|
1666
|
+
return (crop, size)
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/// 사진 썸네일을 만들어 결과에 넣을 필드(thumbnailUri/Width/Height/FileSize)를 돌려줌. options 가 없거나 실패하면 빈 딕셔너리.
|
|
1670
|
+
/// image 는 결과 사진(uri)을 만든 것과 같은 UIImage — 원본을 다시 읽지 않음. 새로 그려 인코딩하므로 EXIF·GPS 는 들어가지 않음
|
|
1671
|
+
private func makePhotoThumbnail(_ image: UIImage, options: PhotoThumbnailOptions?, index: Int,
|
|
1672
|
+
timestamp: Int, tempDir: String) -> [String: Any] {
|
|
1673
|
+
guard let o = options else { return [:] }
|
|
1674
|
+
// 방향(imageOrientation) 반영된 픽셀 크기
|
|
1675
|
+
let w = Int((image.size.width * image.scale).rounded()), h = Int((image.size.height * image.scale).rounded())
|
|
1676
|
+
guard w > 0, h > 0 else { return [:] }
|
|
1677
|
+
let (crop, size) = Self.photoThumbnailLayout(width: w, height: h, options: o)
|
|
1678
|
+
let format = UIGraphicsImageRendererFormat()
|
|
1679
|
+
format.scale = 1
|
|
1680
|
+
format.opaque = false
|
|
1681
|
+
format.preferredRange = .standard
|
|
1682
|
+
let sx = size.width / crop.width, sy = size.height / crop.height
|
|
1683
|
+
let thumb = UIGraphicsImageRenderer(size: size, format: format).image { ctx in
|
|
1684
|
+
ctx.cgContext.interpolationQuality = .high
|
|
1685
|
+
image.draw(in: CGRect(x: -crop.minX * sx, y: -crop.minY * sy, width: CGFloat(w) * sx, height: CGFloat(h) * sy))
|
|
1686
|
+
}
|
|
1687
|
+
// 색 프로필(sRGB ICC, 약 3KB)을 붙이지 않도록 장치 RGB 로 태그 — 수 KB 짜리 썸네일에선 용량의 절반 가까이라.
|
|
1688
|
+
// 프로필이 없는 이미지는 sRGB 로 읽히므로 색은 같음
|
|
1689
|
+
let untagged = thumb.cgImage.flatMap { $0.copy(colorSpace: CGColorSpaceCreateDeviceRGB()) }.map { UIImage(cgImage: $0) } ?? thumb
|
|
1690
|
+
// webp 인코딩이 안 되면 jpg 로
|
|
1691
|
+
var data: Data? = nil
|
|
1692
|
+
var ext = "jpg"
|
|
1693
|
+
if o.format == "webp", let d = encodeWebP(image: untagged, quality: o.quality) { data = d; ext = "webp" }
|
|
1694
|
+
if data == nil { data = untagged.jpegData(compressionQuality: o.quality); ext = "jpg" }
|
|
1695
|
+
guard let bytes = data else { return [:] }
|
|
1696
|
+
let url = URL(fileURLWithPath: (tempDir as NSString).appendingPathComponent("turbo_picker_thumb_\(timestamp)_\(index).\(ext)"))
|
|
1697
|
+
do { try bytes.write(to: url, options: [.atomic]) } catch { return [:] }
|
|
1698
|
+
return [
|
|
1699
|
+
"thumbnailUri": url.absoluteString,
|
|
1700
|
+
"thumbnailWidth": Int(size.width),
|
|
1701
|
+
"thumbnailHeight": Int(size.height),
|
|
1702
|
+
"thumbnailFileSize": bytes.count,
|
|
1703
|
+
]
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1624
1706
|
/// 이미지를 지정 포맷으로 저장하고 file:// URI 반환
|
|
1625
1707
|
/// - Parameters:
|
|
1626
1708
|
/// - image: 저장할 UIImage
|
|
@@ -1686,6 +1768,36 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1686
1768
|
return (fileName, fileExtension, fileSize)
|
|
1687
1769
|
}
|
|
1688
1770
|
|
|
1771
|
+
/// 맨 위 컨트롤러에 present. 맨 위가 닫히거나 열리는 중이면(예: openGallery 가 끝난 직후 갤러리 시트가
|
|
1772
|
+
/// 닫히는 애니메이션) UIKit 이 present 를 조용히 버리므로, 전환이 끝난 뒤 다시 시도합니다.
|
|
1773
|
+
/// 끝내 present 되지 않으면 reject — 안 그러면 JS 약속이 안 풀리고 _editorOpen 가드에 계속 막힙니다.
|
|
1774
|
+
private func presentOnTop(_ vc: UIViewController, reject: @escaping RCTPromiseRejectBlock, attempt: Int = 0) {
|
|
1775
|
+
guard let top = getRootViewController() else {
|
|
1776
|
+
reject("E_NO_ROOT", "Unable to get root view controller", nil)
|
|
1777
|
+
return
|
|
1778
|
+
}
|
|
1779
|
+
if (top.isBeingDismissed || top.isBeingPresented) && attempt < 20 {
|
|
1780
|
+
let retry: () -> Void = { [weak self] in
|
|
1781
|
+
guard let self = self else {
|
|
1782
|
+
reject("E_PRESENT_FAILED", "Module deallocated", nil)
|
|
1783
|
+
return
|
|
1784
|
+
}
|
|
1785
|
+
self.presentOnTop(vc, reject: reject, attempt: attempt + 1)
|
|
1786
|
+
}
|
|
1787
|
+
if let coordinator = top.transitionCoordinator {
|
|
1788
|
+
coordinator.animate(alongsideTransition: nil) { _ in DispatchQueue.main.async(execute: retry) }
|
|
1789
|
+
} else {
|
|
1790
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: retry)
|
|
1791
|
+
}
|
|
1792
|
+
return
|
|
1793
|
+
}
|
|
1794
|
+
top.present(vc, animated: true)
|
|
1795
|
+
// present 가 받아들여지면 presentingViewController 가 바로 잡힘. nil 이면 UIKit 이 거부한 것
|
|
1796
|
+
if vc.presentingViewController == nil {
|
|
1797
|
+
reject("E_PRESENT_FAILED", "Unable to present \(type(of: vc)) on \(type(of: top))", nil)
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1689
1801
|
private func getRootViewController() -> UIViewController? {
|
|
1690
1802
|
var rootViewController: UIViewController?
|
|
1691
1803
|
|
|
@@ -1806,7 +1918,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1806
1918
|
}
|
|
1807
1919
|
|
|
1808
1920
|
|
|
1809
|
-
private func processEditorResult(asset: PHAsset?, croppedImage: UIImage?, filterState: FilterState, caption: String, galleryVC: GalleryViewController?, targetMaxWidth: Int? = nil, targetMaxHeight: Int? = nil, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
1921
|
+
private func processEditorResult(asset: PHAsset?, croppedImage: UIImage?, filterState: FilterState, caption: String, galleryVC: GalleryViewController?, targetMaxWidth: Int? = nil, targetMaxHeight: Int? = nil, thumbnail: PhotoThumbnailOptions? = nil, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
1810
1922
|
let tempDir = prepareCacheDirectory()
|
|
1811
1923
|
// 동영상 1장 편집 → 전송: 편집 화면에서 고른 품질·음소거로 VideoExporter (사진 경로와 같은 결과 형태)
|
|
1812
1924
|
if let phAsset = asset, phAsset.mediaType == .video {
|
|
@@ -1816,6 +1928,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1816
1928
|
let maxWidth = targetMaxWidth ?? galleryVC?.maxWidth ?? 1024
|
|
1817
1929
|
let maxHeight = targetMaxHeight ?? galleryVC?.maxHeight ?? 1024
|
|
1818
1930
|
let format = galleryVC?.outputFormat ?? "webp"
|
|
1931
|
+
// openEditor 는 자기 옵션, 갤러리 편집 경로는 openGallery 의 thumbnail
|
|
1932
|
+
let thumbOptions = thumbnail ?? (galleryVC != nil ? galleryThumbnail : nil)
|
|
1819
1933
|
|
|
1820
1934
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
1821
1935
|
let photoManager = PhotoManager.shared
|
|
@@ -1901,6 +2015,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1901
2015
|
result["fileExtension"] = finalMeta.fileExtension
|
|
1902
2016
|
result["fileSize"] = finalMeta.fileSize
|
|
1903
2017
|
}
|
|
2018
|
+
// 썸네일: 크롭·그리기·필터가 반영된 결과(uri 와 같은 비트맵)에서
|
|
2019
|
+
result.merge(self.makePhotoThumbnail(finalImageForFormat, options: thumbOptions, index: 0,
|
|
2020
|
+
timestamp: timestamp, tempDir: tempDir)) { $1 }
|
|
1904
2021
|
|
|
1905
2022
|
DispatchQueue.main.async {
|
|
1906
2023
|
self.currentGalleryVC = nil
|
|
@@ -1916,6 +2033,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1916
2033
|
let maxWidth = galleryVC.maxWidth
|
|
1917
2034
|
let maxHeight = galleryVC.maxHeight
|
|
1918
2035
|
let format = galleryVC.outputFormat
|
|
2036
|
+
let thumbOptions = galleryThumbnail
|
|
1919
2037
|
|
|
1920
2038
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
1921
2039
|
let photoManager = PhotoManager.shared
|
|
@@ -1962,7 +2080,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1962
2080
|
result["fileSize"] = meta.fileSize
|
|
1963
2081
|
}
|
|
1964
2082
|
|
|
1965
|
-
|
|
2083
|
+
let resizedForThumb = (maxWidth > 0 || maxHeight > 0) ? self.resizeImage(finalImage, maxWidth: maxWidth, maxHeight: maxHeight) : nil
|
|
2084
|
+
if let resizedImage = resizedForThumb {
|
|
1966
2085
|
if let resizedUri = self.saveImage(resizedImage, format: format, prefix: "resized", index: index, timestamp: timestamp, tempDir: tempDir) {
|
|
1967
2086
|
result["uri"] = resizedUri
|
|
1968
2087
|
result["width"] = Int(resizedImage.size.width)
|
|
@@ -1973,6 +2092,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1973
2092
|
result["fileSize"] = resizedMeta.fileSize
|
|
1974
2093
|
}
|
|
1975
2094
|
}
|
|
2095
|
+
result.merge(self.makePhotoThumbnail(resizedForThumb ?? finalImage, options: thumbOptions, index: index,
|
|
2096
|
+
timestamp: timestamp, tempDir: tempDir)) { $1 }
|
|
1976
2097
|
|
|
1977
2098
|
resultsLock.lock()
|
|
1978
2099
|
results.append(result)
|
|
@@ -2018,6 +2139,29 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
2018
2139
|
}
|
|
2019
2140
|
}
|
|
2020
2141
|
|
|
2142
|
+
/// GalleryOptions.thumbnail / EditorOptions.thumbnail
|
|
2143
|
+
struct PhotoThumbnailOptions {
|
|
2144
|
+
let maxWidth: Int
|
|
2145
|
+
let maxHeight: Int
|
|
2146
|
+
/// true = "cover" (상자를 꽉 채우고 가운데 기준으로 자름), false = "fit"
|
|
2147
|
+
let cover: Bool
|
|
2148
|
+
let quality: CGFloat
|
|
2149
|
+
/// "webp" | "jpg"
|
|
2150
|
+
let format: String
|
|
2151
|
+
|
|
2152
|
+
/// RN 옵션 → 값. maxWidth 가 없거나 0 이하면 nil (썸네일 안 만듦)
|
|
2153
|
+
init?(_ raw: Any?) {
|
|
2154
|
+
guard let d = raw as? NSDictionary, let mw = (d["maxWidth"] as? NSNumber)?.intValue, mw > 0 else { return nil }
|
|
2155
|
+
maxWidth = mw
|
|
2156
|
+
let mh = (d["maxHeight"] as? NSNumber)?.intValue ?? 0
|
|
2157
|
+
maxHeight = mh > 0 ? mh : mw
|
|
2158
|
+
cover = (d["mode"] as? String)?.lowercased() == "cover"
|
|
2159
|
+
quality = CGFloat(min(max((d["quality"] as? NSNumber)?.doubleValue ?? 0.8, 0), 1))
|
|
2160
|
+
let f = (d["format"] as? String)?.lowercased()
|
|
2161
|
+
format = (f == "jpg" || f == "jpeg") ? "jpg" : "webp"
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2021
2165
|
// MARK: - UIColor Extension for Hex Support
|
|
2022
2166
|
extension UIColor {
|
|
2023
2167
|
convenience init?(hexString: String) {
|
|
@@ -2059,6 +2203,10 @@ extension RNAttachmentKit {
|
|
|
2059
2203
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
2060
2204
|
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
2061
2205
|
let o = options as? [String: Any] ?? [:]
|
|
2206
|
+
// 동영상 항목은 라이선스 키가 있어야
|
|
2207
|
+
if !LicenseGate.isLicensed, (o["items"] as? [[String: Any]] ?? []).compactMap({ MediaGalleryItem(dictionary: $0) }).contains(where: { $0.isVideo }) {
|
|
2208
|
+
reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
|
|
2209
|
+
}
|
|
2062
2210
|
DispatchQueue.main.async {
|
|
2063
2211
|
let config = MediaGalleryConfig(dictionary: o)
|
|
2064
2212
|
let galleryId = config.galleryId
|
|
@@ -2140,6 +2288,9 @@ extension RNAttachmentKit {
|
|
|
2140
2288
|
let galleryId = p["galleryId"] as? String ?? ""
|
|
2141
2289
|
let raw = p["items"] as? [[String: Any]] ?? []
|
|
2142
2290
|
let items = raw.compactMap { MediaGalleryItem(dictionary: $0) }
|
|
2291
|
+
if !LicenseGate.isLicensed, items.contains(where: { $0.isVideo }) {
|
|
2292
|
+
reject(LicenseGate.errorCode, LicenseGate.errorMessage, nil); return
|
|
2293
|
+
}
|
|
2143
2294
|
let page = MediaGalleryPage(items: items, nextCursor: p["nextCursor"] as? String,
|
|
2144
2295
|
hasMore: (p["hasMore"] as? NSNumber)?.boolValue ?? false)
|
|
2145
2296
|
DispatchQueue.main.async {
|