react-native-attachment-kit 1.0.5 → 1.0.6
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 +100 -3
- package/android/build.gradle +14 -0
- package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
- package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +280 -13
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +282 -13
- package/ios/RNAttachmentKit.m +18 -0
- package/ios/RNAttachmentKit.swift +421 -9
- package/ios/RNAttachmentKit.xcframework/Info.plist +5 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Headers/RNAttachmentKit-Swift.h +21 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Info.plist +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +33275 -7562
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +193 -5
- 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 +193 -5
- 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 +15 -15
- 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/Headers/RNAttachmentKit-Swift.h +42 -10
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Info.plist +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +33275 -7562
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +193 -5
- 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 +193 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +33275 -7562
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +193 -5
- 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 +193 -5
- 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 +25 -25
- package/package.json +1 -1
- package/react-native-attachment-kit.podspec +1 -1
- package/src/NativeAttachmentKit.ts +5 -0
- package/src/index.ts +118 -7
- package/src/types.ts +173 -0
|
@@ -49,6 +49,13 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
49
49
|
private var editorTransitionDelegate: ImageEditorTransitionDelegate?
|
|
50
50
|
/// 열려 있는 미디어 갤러리 (galleryId → 화면)
|
|
51
51
|
fileprivate var mediaGalleries: [String: MediaGalleryViewController] = [:]
|
|
52
|
+
/// 진행 중인 전송 처리 (processingId → 작업, 메인 스레드에서만). 보내는 중에 갤러리를 다시 열어 또 보낼 수 있어 여러 개일 수 있음
|
|
53
|
+
private var processingJobs: [String: MediaProcessingJob] = [:]
|
|
54
|
+
/// 가장 최근에 시작한 처리 (cancelVideoProcessing 에 processingId 가 없을 때)
|
|
55
|
+
private var lastProcessingId: String?
|
|
56
|
+
/// 동영상 변환 진행 HUD 를 띄울지 — video.showProgressHUD, 없으면 asyncProcessing 이 아닐 때만
|
|
57
|
+
/// (앱이 onProcessingStart / onVideoProgress / onImageProcessed 로 말풍선에 진행 상황을 그리는 구조면 HUD 없이)
|
|
58
|
+
private var galleryShowsVideoHUD = true
|
|
52
59
|
|
|
53
60
|
// MARK: - Module Setup
|
|
54
61
|
|
|
@@ -66,7 +73,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
66
73
|
override func supportedEvents() -> [String]! {
|
|
67
74
|
return ["onSelectionChange", "onImageProcessed", "onPageSelected", "onViewerWillClose", "onImageDeleted",
|
|
68
75
|
"onGalleryItemDeleted", "onGalleryNeedPage", "onGalleryClosed", "onGalleryGridSizeChanged",
|
|
69
|
-
"onLocationSelected", "onFilesSelected"
|
|
76
|
+
"onLocationSelected", "onFilesSelected", "onAIUsage", "onAIChargeRequired", "onVideoProgress",
|
|
77
|
+
"onMapViewerWillClose", "onProcessingStart"]
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
// MARK: - Public Methods
|
|
@@ -270,6 +278,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
270
278
|
vc.sourceImageIsMarkerless = markerless && image != nil
|
|
271
279
|
vc.sourceRectProvider = { [weak self] in self?.mapViewerSourceRect }
|
|
272
280
|
}
|
|
281
|
+
vc.onWillClose = { [weak self] dur in
|
|
282
|
+
self?.sendEvent(withName: "onMapViewerWillClose", body: ["durationMs": Int(dur * 1000)])
|
|
283
|
+
}
|
|
273
284
|
vc.onClosed = { [weak self] in
|
|
274
285
|
self?.mapViewerSourceRect = nil
|
|
275
286
|
resolve(nil)
|
|
@@ -365,6 +376,20 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
365
376
|
}
|
|
366
377
|
}
|
|
367
378
|
|
|
379
|
+
/// AI 기능 설정·잔액 (넘긴 값만 바뀜). 사용·충전 요청은 onAIUsage / onAIChargeRequired 이벤트로 알림
|
|
380
|
+
@objc
|
|
381
|
+
func setAIConfig(_ config: NSDictionary,
|
|
382
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
383
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
384
|
+
let dict = config as? [String: Any] ?? [:]
|
|
385
|
+
DispatchQueue.main.async {
|
|
386
|
+
AIBilling.apply(dict)
|
|
387
|
+
AIBilling.onUsage = { [weak self] body in self?.sendEvent(withName: "onAIUsage", body: body) }
|
|
388
|
+
AIBilling.onChargeRequired = { [weak self] body in self?.sendEvent(withName: "onAIChargeRequired", body: body) }
|
|
389
|
+
resolve(nil)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
368
393
|
@objc
|
|
369
394
|
func clearReverseGeocodeCache(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
370
395
|
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
@@ -393,6 +418,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
393
418
|
let placeholderImages = options["placeholderImages"] as? [String]
|
|
394
419
|
// cacheFiles: images 와 같은 순서의 로컬 캐시 파일 경로 (null 항목은 NSNull 로 옴 → 빈 문자열)
|
|
395
420
|
let cacheFiles: [String]? = (options["cacheFiles"] as? [Any])?.map { ($0 as? String) ?? "" }
|
|
421
|
+
// videos: images 와 같은 순서. 항목이 있으면 그 페이지는 동영상 (images[i] 는 포스터)
|
|
422
|
+
let viewerVideos: [ViewerVideoItem?] = (options["videos"] as? [Any])?.map { ViewerVideoItem(dictionary: $0) } ?? []
|
|
396
423
|
let themeColorHex = options["themeColor"] as? String
|
|
397
424
|
let languageCode = options["languageCode"] as? String ?? "en"
|
|
398
425
|
let viewerTitle = options["title"] as? String
|
|
@@ -476,6 +503,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
476
503
|
viewerVC.viewerTitle = viewerTitle
|
|
477
504
|
viewerVC.placeholderImages = placeholderImages
|
|
478
505
|
viewerVC.cacheFiles = cacheFiles
|
|
506
|
+
viewerVC.videos = viewerVideos
|
|
479
507
|
if let hex = themeColorHex, let color = UIColor(hexString: hex) {
|
|
480
508
|
viewerVC.themeColor = color
|
|
481
509
|
}
|
|
@@ -941,6 +969,14 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
941
969
|
galleryVC.showsAttachMenu = options["showAttachMenu"] as? Bool ?? false
|
|
942
970
|
// 위치 보내기 때 지도 캡처 이미지 (기본 true). false 면 위치 정보만 — 받는 쪽이 renderLocationThumbnail 로 그림
|
|
943
971
|
galleryVC.capturesLocationImage = options["captureLocationImage"] as? Bool ?? true
|
|
972
|
+
// 동영상: mediaType "photo"(기본) | "video" | "all" + video 옵션 (지시서 3-1)
|
|
973
|
+
let mediaTypeOption = (options["mediaType"] as? String)?.lowercased() ?? "photo"
|
|
974
|
+
galleryVC.mediaType = ["photo", "video", "all"].contains(mediaTypeOption) ? mediaTypeOption : "photo"
|
|
975
|
+
let videoOptions = VideoExportOptions(dictionary: options["video"] as? [String: Any])
|
|
976
|
+
let asyncProcessing = options["asyncProcessing"] as? Bool ?? false
|
|
977
|
+
self.galleryShowsVideoHUD = ((options["video"] as? [String: Any])?["showProgressHUD"] as? Bool) ?? !asyncProcessing
|
|
978
|
+
galleryVC.maxVideoDurationSec = videoOptions.maxDurationSec
|
|
979
|
+
galleryVC.videoExportOptions = videoOptions
|
|
944
980
|
// 위치 탭에서 보내기 → onLocationSelected 이벤트로 결과를 넘기고, openGallery 는 빈 배열로 끝냄
|
|
945
981
|
galleryVC.onLocationSelected = { [weak self, weak galleryVC] location in
|
|
946
982
|
self?.currentGalleryVC = nil
|
|
@@ -965,6 +1001,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
965
1001
|
let galleryMaxWidth = galleryVC.maxWidth
|
|
966
1002
|
let galleryMaxHeight = galleryVC.maxHeight
|
|
967
1003
|
let galleryFormat = galleryVC.outputFormat // "webp" | "jpg" | "png"
|
|
1004
|
+
let galleryAccent = galleryVC.parsedThemeColor
|
|
1005
|
+
let galleryLanguage = galleryVC.languageCode
|
|
968
1006
|
galleryVC.onImagesSelected = { [weak self, weak galleryVC] selectedImages in
|
|
969
1007
|
guard let self = self else { resolve([]); return }
|
|
970
1008
|
|
|
@@ -987,16 +1025,126 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
987
1025
|
// 처리한 이미지는 바로 놓아 주도록 (전체 UIImage 배열을 끝까지 들고 있지 않게) 옵셔널 배열로 옮김
|
|
988
1026
|
var pending: [(PHAsset?, UIImage)?] = selectedImages.map { $0 }
|
|
989
1027
|
|
|
1028
|
+
// 동영상이 섞여 있으면 변환 진행 표시 (갤러리가 닫힌 뒤에도 보이게 키 윈도우에) + 취소
|
|
1029
|
+
let hasVideo = selectedImages.contains { $0.0?.mediaType == .video }
|
|
1030
|
+
// 편집 화면에서 고른 품질·음소거 (없으면 기본 품질)
|
|
1031
|
+
let videoStates = galleryVC?.videoEditStates ?? [:]
|
|
1032
|
+
var hud: VideoExportHUD?
|
|
1033
|
+
var currentExporter: VideoExporter?
|
|
1034
|
+
var cancelledAll = false
|
|
1035
|
+
let exporterLock = NSLock()
|
|
1036
|
+
// 앱이 동영상 하나(또는 전부)를 취소할 수 있게 (cancelVideoProcessing)
|
|
1037
|
+
let job = MediaProcessingJob()
|
|
1038
|
+
let processingId = UUID().uuidString
|
|
1039
|
+
self.processingJobs[processingId] = job
|
|
1040
|
+
self.lastProcessingId = processingId
|
|
1041
|
+
// 처리 시작 이벤트용 — 고른 순서의 원본(사진은 편집 결과) 크기·포스터
|
|
1042
|
+
let startItems = selectedImages.map { ($0.0, $0.1) }
|
|
1043
|
+
if hasVideo && self.galleryShowsVideoHUD {
|
|
1044
|
+
hud = VideoExportHUD.show(accent: galleryAccent, languageCode: galleryLanguage)
|
|
1045
|
+
hud?.onCancel = {
|
|
1046
|
+
exporterLock.lock(); cancelledAll = true; currentExporter?.cancel(); exporterLock.unlock()
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
990
1050
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
1051
|
+
// ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
|
|
1052
|
+
self.sendEvent(withName: "onProcessingStart", body: [
|
|
1053
|
+
"processingId": processingId,
|
|
1054
|
+
"total": total,
|
|
1055
|
+
"items": startItems.enumerated().map { i, pair in
|
|
1056
|
+
self.processingStartItem(index: i, asset: pair.0, image: pair.1, videoStates: videoStates,
|
|
1057
|
+
videoOptions: videoOptions, maxWidth: maxWidth, maxHeight: maxHeight,
|
|
1058
|
+
tempDir: tempDir)
|
|
1059
|
+
},
|
|
1060
|
+
])
|
|
991
1061
|
for index in 0..<total {
|
|
992
1062
|
guard let pair = pending[index] else { continue }
|
|
993
1063
|
pending[index] = nil
|
|
1064
|
+
exporterLock.lock(); let stop = cancelledAll; exporterLock.unlock()
|
|
1065
|
+
if stop { break }
|
|
994
1066
|
autoreleasepool {
|
|
995
1067
|
let (asset, image) = pair
|
|
996
1068
|
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
997
1069
|
let originalSize = image.size
|
|
998
1070
|
|
|
999
|
-
if let phAsset = asset {
|
|
1071
|
+
if let phAsset = asset, phAsset.mediaType == .video {
|
|
1072
|
+
// ✅ 동영상: VideoExporter 로 전송용 mp4 (+ 썸네일·메타). image 는 포스터(목록용)
|
|
1073
|
+
let exporter = VideoExporter()
|
|
1074
|
+
exporterLock.lock(); currentExporter = exporter; exporterLock.unlock()
|
|
1075
|
+
let base = "turbo_video_\(timestamp)_\(index)"
|
|
1076
|
+
var result: [String: Any] = [
|
|
1077
|
+
"originalUri": "ph://\(phAsset.localIdentifier)",
|
|
1078
|
+
"originalWidth": phAsset.pixelWidth,
|
|
1079
|
+
"originalHeight": phAsset.pixelHeight,
|
|
1080
|
+
"mediaType": "video",
|
|
1081
|
+
"type": "video/mp4",
|
|
1082
|
+
"duration": phAsset.duration,
|
|
1083
|
+
]
|
|
1084
|
+
do {
|
|
1085
|
+
// 앱이 이미 취소한 동영상은 건너뜀 (결과에 cancelled 로)
|
|
1086
|
+
if job.isCancelled(index) { throw VideoExporter.ExportError.cancelled }
|
|
1087
|
+
job.begin(index: index, exporter: exporter)
|
|
1088
|
+
let state = videoStates[phAsset.localIdentifier] ?? VideoEditState.defaultState(for: phAsset, options: videoOptions)
|
|
1089
|
+
let throttle = ProgressThrottle()
|
|
1090
|
+
let onProgress: (Double) -> Void = { p in
|
|
1091
|
+
// 프레임마다 메인으로 보내지 않고 1%·0.1초 단위로만 (HUD 표시는 정수 % 라 같음)
|
|
1092
|
+
guard throttle.shouldSend(p) else { return }
|
|
1093
|
+
DispatchQueue.main.async { hud?.update(progress: p, index: index, total: total) }
|
|
1094
|
+
self.sendEvent(withName: "onVideoProgress", body: ["index": index, "total": total, "progress": p, "processingId": processingId])
|
|
1095
|
+
}
|
|
1096
|
+
// 편집 화면에서 미리 만들어 둔 파일이 있으면(또는 만드는 중이면 끝나고) 그대로 씀
|
|
1097
|
+
let r: VideoExportResult
|
|
1098
|
+
// 기다리는 동안 앱(X)이나 HUD 가 취소하면 바로 멈춤 (전엔 미리 만들기가 끝날 때까지 못 빠져나왔음)
|
|
1099
|
+
let stop = { () -> Bool in
|
|
1100
|
+
exporterLock.lock(); let all = cancelledAll; exporterLock.unlock()
|
|
1101
|
+
return all || job.isCancelled(index)
|
|
1102
|
+
}
|
|
1103
|
+
if let ready = VideoPrepareManager.shared.takeReady(asset: phAsset, state: state, progress: onProgress, isCancelled: stop) {
|
|
1104
|
+
r = ready; onProgress(1)
|
|
1105
|
+
} else {
|
|
1106
|
+
if stop() { throw VideoExporter.ExportError.cancelled }
|
|
1107
|
+
r = try exporter.export(asset: phAsset, state: state,
|
|
1108
|
+
options: videoOptions, outputDir: URL(fileURLWithPath: tempDir), baseName: base, progress: onProgress)
|
|
1109
|
+
}
|
|
1110
|
+
// 미리 만든 파일을 기다리는 동안 취소됐으면 결과를 쓰지 않음
|
|
1111
|
+
if job.isCancelled(index) { throw VideoExporter.ExportError.cancelled }
|
|
1112
|
+
result["uri"] = "file://" + r.url.path
|
|
1113
|
+
result["width"] = r.width
|
|
1114
|
+
result["height"] = r.height
|
|
1115
|
+
result["duration"] = r.duration
|
|
1116
|
+
result["fileSize"] = r.fileSize
|
|
1117
|
+
result["fileName"] = r.url.lastPathComponent
|
|
1118
|
+
result["fileExtension"] = "mp4"
|
|
1119
|
+
result["videoCodec"] = r.codec
|
|
1120
|
+
result["videoBitrate"] = r.bitrate
|
|
1121
|
+
result["edited"] = !r.passthrough
|
|
1122
|
+
if let t = r.thumbnailURL {
|
|
1123
|
+
result["thumbnailUri"] = "file://" + t.path
|
|
1124
|
+
result["thumbnailWidth"] = r.thumbnailWidth
|
|
1125
|
+
result["thumbnailHeight"] = r.thumbnailHeight
|
|
1126
|
+
}
|
|
1127
|
+
} catch VideoExporter.ExportError.cancelled where job.isCancelled(index) {
|
|
1128
|
+
// 앱이 이 동영상만 취소 → 나머지는 계속
|
|
1129
|
+
result["error"] = "CANCELLED"
|
|
1130
|
+
result["cancelled"] = true
|
|
1131
|
+
} catch VideoExporter.ExportError.cancelled {
|
|
1132
|
+
// HUD 의 취소 → 전체 취소 (Promise reject)
|
|
1133
|
+
job.end()
|
|
1134
|
+
exporterLock.lock(); cancelledAll = true; exporterLock.unlock()
|
|
1135
|
+
return
|
|
1136
|
+
} catch {
|
|
1137
|
+
result["error"] = error.localizedDescription
|
|
1138
|
+
}
|
|
1139
|
+
job.end()
|
|
1140
|
+
exporterLock.lock(); currentExporter = nil; exporterLock.unlock()
|
|
1141
|
+
|
|
1142
|
+
resultsLock.lock()
|
|
1143
|
+
results.append(result)
|
|
1144
|
+
let currentResult = result
|
|
1145
|
+
resultsLock.unlock()
|
|
1146
|
+
self.sendEvent(withName: "onImageProcessed", body: ["index": index, "total": total, "image": currentResult, "processingId": processingId])
|
|
1147
|
+
} else if let phAsset = asset {
|
|
1000
1148
|
// ✅ 1순위: PHAsset
|
|
1001
1149
|
let originalUri = "ph://\(phAsset.localIdentifier)"
|
|
1002
1150
|
|
|
@@ -1036,7 +1184,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1036
1184
|
self.sendEvent(withName: "onImageProcessed", body: [
|
|
1037
1185
|
"index": index,
|
|
1038
1186
|
"total": total,
|
|
1039
|
-
"image": currentResult
|
|
1187
|
+
"image": currentResult,
|
|
1188
|
+
"processingId": processingId
|
|
1040
1189
|
])
|
|
1041
1190
|
} else {
|
|
1042
1191
|
// ✅ 2순위: 카메라 촬영본
|
|
@@ -1086,17 +1235,30 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1086
1235
|
self.sendEvent(withName: "onImageProcessed", body: [
|
|
1087
1236
|
"index": index,
|
|
1088
1237
|
"total": total,
|
|
1089
|
-
"image": currentResult
|
|
1238
|
+
"image": currentResult,
|
|
1239
|
+
"processingId": processingId
|
|
1090
1240
|
])
|
|
1091
1241
|
}
|
|
1092
1242
|
} // end autoreleasepool
|
|
1093
1243
|
} // end for
|
|
1094
1244
|
|
|
1095
1245
|
DispatchQueue.main.async {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1246
|
+
hud?.dismiss()
|
|
1247
|
+
// 처리하는 동안 새 갤러리가 열렸을 수 있음 → 내 갤러리일 때만 비움
|
|
1248
|
+
if self.currentGalleryVC == nil || self.currentGalleryVC === galleryVC { self.currentGalleryVC = nil }
|
|
1249
|
+
self.processingJobs[processingId] = nil
|
|
1250
|
+
exporterLock.lock(); let wasCancelled = cancelledAll; exporterLock.unlock()
|
|
1251
|
+
if wasCancelled {
|
|
1252
|
+
// 변환 취소: 이미 만든 파일은 버림
|
|
1253
|
+
results.compactMap { ($0["uri"] as? String)?.replacingOccurrences(of: "file://", with: "") }
|
|
1254
|
+
.forEach { try? FileManager.default.removeItem(atPath: $0) }
|
|
1255
|
+
reject("CANCELLED", "User cancelled video conversion", nil)
|
|
1256
|
+
} else {
|
|
1257
|
+
resolve(results)
|
|
1258
|
+
}
|
|
1259
|
+
// 결과를 넘긴 뒤: 갤러리의 PHCachingImageManager 캐시·콜백 해제 (닫힘 애니메이션에는 영향 없음).
|
|
1260
|
+
// 새 갤러리가 열려 있으면 그 화면이 미리 인코딩 중인 파일은 건드리지 않음
|
|
1261
|
+
galleryVC?.releaseAfterReturn(discardPreparedVideos: self.currentGalleryVC == nil)
|
|
1100
1262
|
}
|
|
1101
1263
|
}
|
|
1102
1264
|
}
|
|
@@ -1150,6 +1312,75 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1150
1312
|
}
|
|
1151
1313
|
}
|
|
1152
1314
|
|
|
1315
|
+
/// 동영상 변환 취소 (HUD 없이 앱이 말풍선에서 취소할 때). index 생략(-1) = 남은 동영상 전부.
|
|
1316
|
+
/// 취소된 동영상은 onImageProcessed / 결과에 { error: "CANCELLED", cancelled: true } 로 오고, 나머지는 계속 처리
|
|
1317
|
+
/// processingId 생략("") = 가장 최근 처리
|
|
1318
|
+
@objc(cancelVideoProcessing:processingId:resolve:reject:)
|
|
1319
|
+
func cancelVideoProcessing(_ index: NSNumber, processingId: NSString, resolve: @escaping RCTPromiseResolveBlock,
|
|
1320
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
1321
|
+
DispatchQueue.main.async {
|
|
1322
|
+
let id = processingId.length > 0 ? processingId as String : self.lastProcessingId
|
|
1323
|
+
guard let id = id, let job = self.processingJobs[id] else { resolve(false); return }
|
|
1324
|
+
let i = index.intValue
|
|
1325
|
+
job.cancel(index: i >= 0 ? i : nil)
|
|
1326
|
+
resolve(true)
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/// onProcessingStart 의 한 자리: 사진은 예상 결과 크기, 동영상은 예상 결과 크기·길이(구간 반영)·포스터(크롭·그리기 반영 jpg)
|
|
1331
|
+
private func processingStartItem(index: Int, asset: PHAsset?, image: UIImage?, videoStates: [String: VideoEditState],
|
|
1332
|
+
videoOptions: VideoExportOptions, maxWidth: Int, maxHeight: Int, tempDir: String) -> [String: Any] {
|
|
1333
|
+
if let a = asset, a.mediaType == .video {
|
|
1334
|
+
let state = videoStates[a.localIdentifier] ?? VideoEditState.defaultState(for: a, options: videoOptions)
|
|
1335
|
+
let d = CGSize(width: a.pixelWidth, height: a.pixelHeight)
|
|
1336
|
+
let size = VideoExporter.predictedOutputSize(displaySize: d, state: state, options: videoOptions, sourceDuration: a.duration)
|
|
1337
|
+
var item: [String: Any] = [
|
|
1338
|
+
"index": index, "mediaType": "video", "originalUri": "ph://\(a.localIdentifier)",
|
|
1339
|
+
"width": Int(size.width), "height": Int(size.height),
|
|
1340
|
+
"duration": state.outputDuration(sourceDuration: a.duration),
|
|
1341
|
+
]
|
|
1342
|
+
if let poster = image {
|
|
1343
|
+
let edited = state.applyEdits(to: poster)
|
|
1344
|
+
if let encoded = VideoExporter.encodePoster(edited, format: videoOptions.posterFormat, quality: 0.8) {
|
|
1345
|
+
let path = (tempDir as NSString).appendingPathComponent("turbo_video_poster_\(Int(Date().timeIntervalSince1970 * 1000))_\(index).\(encoded.fileExtension)")
|
|
1346
|
+
if (try? encoded.data.write(to: URL(fileURLWithPath: path))) != nil {
|
|
1347
|
+
item["thumbnailUri"] = "file://" + path
|
|
1348
|
+
item["thumbnailWidth"] = Int(edited.size.width * edited.scale)
|
|
1349
|
+
item["thumbnailHeight"] = Int(edited.size.height * edited.scale)
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return item
|
|
1354
|
+
}
|
|
1355
|
+
// 사진: 리사이즈 규칙(resizeImage)과 같은 예상 크기 (resizeImage 처럼 image.size 기준)
|
|
1356
|
+
var w = image?.size.width ?? CGFloat(asset?.pixelWidth ?? 0)
|
|
1357
|
+
var h = image?.size.height ?? CGFloat(asset?.pixelHeight ?? 0)
|
|
1358
|
+
if w > 0, h > 0 {
|
|
1359
|
+
var ratio: CGFloat = 1
|
|
1360
|
+
if maxWidth > 0 && maxHeight > 0 { ratio = min(CGFloat(maxWidth) / w, CGFloat(maxHeight) / h, 1) }
|
|
1361
|
+
else if maxWidth > 0 { ratio = min(CGFloat(maxWidth) / w, 1) }
|
|
1362
|
+
else if maxHeight > 0 { ratio = min(CGFloat(maxHeight) / h, 1) }
|
|
1363
|
+
w *= ratio; h *= ratio
|
|
1364
|
+
}
|
|
1365
|
+
var item: [String: Any] = ["index": index, "mediaType": "image", "width": Int(w.rounded()), "height": Int(h.rounded())]
|
|
1366
|
+
if let a = asset { item["originalUri"] = "ph://\(a.localIdentifier)" }
|
|
1367
|
+
return item
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/// 동영상 포스터 (긴 변 512, 동기) — 처리 시작 이벤트용
|
|
1371
|
+
private static func requestPoster(_ asset: PHAsset) -> UIImage? {
|
|
1372
|
+
let o = PHImageRequestOptions()
|
|
1373
|
+
o.isSynchronous = true
|
|
1374
|
+
o.deliveryMode = .highQualityFormat
|
|
1375
|
+
o.resizeMode = .fast
|
|
1376
|
+
o.isNetworkAccessAllowed = true
|
|
1377
|
+
var out: UIImage?
|
|
1378
|
+
PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 512, height: 512), contentMode: .aspectFit, options: o) { img, _ in
|
|
1379
|
+
out = img
|
|
1380
|
+
}
|
|
1381
|
+
return out
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1153
1384
|
@objc
|
|
1154
1385
|
func closeGallery(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1155
1386
|
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
@@ -1457,9 +1688,105 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1457
1688
|
}
|
|
1458
1689
|
|
|
1459
1690
|
// MARK: - Editor Result Processing
|
|
1691
|
+
|
|
1692
|
+
/// 동영상 1장을 전송용 mp4 로 만들어 resolve (갤러리 다중 전송 경로의 동영상 분기와 같은 필드)
|
|
1693
|
+
private func exportSingleVideo(_ phAsset: PHAsset, galleryVC: GalleryViewController?, tempDir: String,
|
|
1694
|
+
resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
1695
|
+
let videoOptions = galleryVC?.videoExportOptions ?? VideoExportOptions()
|
|
1696
|
+
let state = galleryVC?.videoEditStates[phAsset.localIdentifier] ?? VideoEditState.defaultState(for: phAsset, options: videoOptions)
|
|
1697
|
+
let hud: VideoExportHUD? = galleryShowsVideoHUD ? VideoExportHUD.show(accent: galleryVC?.parsedThemeColor ?? UIColor(red: 236/255, green: 73/255, blue: 38/255, alpha: 1), languageCode: galleryVC?.languageCode ?? "en") : nil
|
|
1698
|
+
let exporter = VideoExporter()
|
|
1699
|
+
let hudCancelled = AtomicFlag()
|
|
1700
|
+
hud?.onCancel = { hudCancelled.value = true; exporter.cancel() }
|
|
1701
|
+
let job = MediaProcessingJob()
|
|
1702
|
+
let processingId = UUID().uuidString
|
|
1703
|
+
processingJobs[processingId] = job
|
|
1704
|
+
lastProcessingId = processingId
|
|
1705
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
1706
|
+
let poster = Self.requestPoster(phAsset)
|
|
1707
|
+
self.sendEvent(withName: "onProcessingStart", body: [
|
|
1708
|
+
"processingId": processingId,
|
|
1709
|
+
"total": 1,
|
|
1710
|
+
"items": [self.processingStartItem(index: 0, asset: phAsset, image: poster, videoStates: [phAsset.localIdentifier: state],
|
|
1711
|
+
videoOptions: videoOptions, maxWidth: 0, maxHeight: 0, tempDir: tempDir)],
|
|
1712
|
+
])
|
|
1713
|
+
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
1714
|
+
var result: [String: Any] = [
|
|
1715
|
+
"originalUri": "ph://\(phAsset.localIdentifier)",
|
|
1716
|
+
"originalWidth": phAsset.pixelWidth,
|
|
1717
|
+
"originalHeight": phAsset.pixelHeight,
|
|
1718
|
+
"mediaType": "video",
|
|
1719
|
+
"type": "video/mp4",
|
|
1720
|
+
"duration": phAsset.duration,
|
|
1721
|
+
]
|
|
1722
|
+
do {
|
|
1723
|
+
job.begin(index: 0, exporter: exporter)
|
|
1724
|
+
let throttle = ProgressThrottle()
|
|
1725
|
+
let onProgress: (Double) -> Void = { p in
|
|
1726
|
+
guard throttle.shouldSend(p) else { return }
|
|
1727
|
+
DispatchQueue.main.async { hud?.update(progress: p, index: 0, total: 1) }
|
|
1728
|
+
self.sendEvent(withName: "onVideoProgress", body: ["index": 0, "total": 1, "progress": p, "processingId": processingId])
|
|
1729
|
+
}
|
|
1730
|
+
let r: VideoExportResult
|
|
1731
|
+
// HUD 취소는 exporter.cancel() — 기다리는 동안엔 job 으로 알 수 없으므로 HUD 취소도 job 에 기록
|
|
1732
|
+
if let ready = VideoPrepareManager.shared.takeReady(asset: phAsset, state: state, progress: onProgress,
|
|
1733
|
+
isCancelled: { job.isCancelled(0) || hudCancelled.value }) {
|
|
1734
|
+
r = ready; onProgress(1)
|
|
1735
|
+
} else {
|
|
1736
|
+
if job.isCancelled(0) || hudCancelled.value { throw VideoExporter.ExportError.cancelled }
|
|
1737
|
+
r = try exporter.export(asset: phAsset, state: state, options: videoOptions,
|
|
1738
|
+
outputDir: URL(fileURLWithPath: tempDir), baseName: "turbo_video_\(timestamp)_0", progress: onProgress)
|
|
1739
|
+
}
|
|
1740
|
+
result["uri"] = "file://" + r.url.path
|
|
1741
|
+
result["width"] = r.width
|
|
1742
|
+
result["height"] = r.height
|
|
1743
|
+
result["duration"] = r.duration
|
|
1744
|
+
result["fileSize"] = r.fileSize
|
|
1745
|
+
result["fileName"] = r.url.lastPathComponent
|
|
1746
|
+
result["fileExtension"] = "mp4"
|
|
1747
|
+
result["videoCodec"] = r.codec
|
|
1748
|
+
result["videoBitrate"] = r.bitrate
|
|
1749
|
+
result["edited"] = !r.passthrough
|
|
1750
|
+
if let t = r.thumbnailURL {
|
|
1751
|
+
result["thumbnailUri"] = "file://" + t.path
|
|
1752
|
+
result["thumbnailWidth"] = r.thumbnailWidth
|
|
1753
|
+
result["thumbnailHeight"] = r.thumbnailHeight
|
|
1754
|
+
}
|
|
1755
|
+
if job.isCancelled(0) { throw VideoExporter.ExportError.cancelled }
|
|
1756
|
+
} catch VideoExporter.ExportError.cancelled where job.isCancelled(0) {
|
|
1757
|
+
// 앱이 취소 (cancelVideoProcessing) — 이벤트·결과에 cancelled 로
|
|
1758
|
+
result["error"] = "CANCELLED"
|
|
1759
|
+
result["cancelled"] = true
|
|
1760
|
+
} catch VideoExporter.ExportError.cancelled {
|
|
1761
|
+
job.end()
|
|
1762
|
+
DispatchQueue.main.async {
|
|
1763
|
+
self.processingJobs[processingId] = nil
|
|
1764
|
+
hud?.dismiss(); reject("CANCELLED", "Video export cancelled", nil)
|
|
1765
|
+
}
|
|
1766
|
+
return
|
|
1767
|
+
} catch {
|
|
1768
|
+
result["error"] = error.localizedDescription
|
|
1769
|
+
}
|
|
1770
|
+
job.end()
|
|
1771
|
+
self.sendEvent(withName: "onImageProcessed", body: ["index": 0, "total": 1, "image": result, "processingId": processingId])
|
|
1772
|
+
DispatchQueue.main.async {
|
|
1773
|
+
hud?.dismiss()
|
|
1774
|
+
if self.currentGalleryVC == nil || self.currentGalleryVC === galleryVC { self.currentGalleryVC = nil }
|
|
1775
|
+
self.processingJobs[processingId] = nil
|
|
1776
|
+
galleryVC?.releaseAfterReturn(discardPreparedVideos: self.currentGalleryVC == nil)
|
|
1777
|
+
resolve([result])
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1460
1782
|
|
|
1461
1783
|
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) {
|
|
1462
1784
|
let tempDir = prepareCacheDirectory()
|
|
1785
|
+
// 동영상 1장 편집 → 전송: 편집 화면에서 고른 품질·음소거로 VideoExporter (사진 경로와 같은 결과 형태)
|
|
1786
|
+
if let phAsset = asset, phAsset.mediaType == .video {
|
|
1787
|
+
exportSingleVideo(phAsset, galleryVC: galleryVC, tempDir: tempDir, resolve: resolve, reject: reject)
|
|
1788
|
+
return
|
|
1789
|
+
}
|
|
1463
1790
|
let maxWidth = targetMaxWidth ?? galleryVC?.maxWidth ?? 1024
|
|
1464
1791
|
let maxHeight = targetMaxHeight ?? galleryVC?.maxHeight ?? 1024
|
|
1465
1792
|
let format = galleryVC?.outputFormat ?? "webp"
|
|
@@ -1906,6 +2233,7 @@ fileprivate extension RNAttachmentKit {
|
|
|
1906
2233
|
|
|
1907
2234
|
editorVC.onEditDeleted = { [weak currentGallery] asset in
|
|
1908
2235
|
currentGallery?.editedImages.removeValue(forKey: asset.localIdentifier)
|
|
2236
|
+
currentGallery?.videoEditStates.removeValue(forKey: asset.localIdentifier) // 동영상 편집도 기본으로 돌아간 것
|
|
1909
2237
|
}
|
|
1910
2238
|
|
|
1911
2239
|
editorVC.modalPresentationStyle = .overFullScreen
|
|
@@ -1921,7 +2249,22 @@ fileprivate extension RNAttachmentKit {
|
|
|
1921
2249
|
}
|
|
1922
2250
|
}
|
|
1923
2251
|
editorVC.croppedImages = initialCroppedImages
|
|
2252
|
+
// 동영상 페이지: 품질 단계·용량 상한 + 갤러리가 들고 있는 상태. 바뀌면 바로 갤러리에 반영 (전송 때 VideoExporter 가 읽음)
|
|
2253
|
+
editorVC.videoOptions = VideoExportOptions(dictionary: options["video"] as? [String: Any])
|
|
2254
|
+
var initialVideoStates: [Int: VideoEditState] = [:]
|
|
2255
|
+
for (i, a) in editorAssets.enumerated() {
|
|
2256
|
+
if let st = currentGallery.videoEditStates[a.localIdentifier] { initialVideoStates[i] = st }
|
|
2257
|
+
}
|
|
2258
|
+
editorVC.videoEditStates = initialVideoStates
|
|
2259
|
+
editorVC.onVideoStateChanged = { [weak currentGallery] asset, state in
|
|
2260
|
+
currentGallery?.videoEditStates[asset.localIdentifier] = state
|
|
2261
|
+
}
|
|
1924
2262
|
|
|
2263
|
+
if sourceFrame == .zero {
|
|
2264
|
+
// 기준 칸이 없을 때(칸이 화면 밖 등): 기본 전환(아래로 미끄러짐)은 끌어내려 닫을 때 위로 치워 둔 상단 바가
|
|
2265
|
+
// 화면과 함께 내려오며 보이므로 페이드로
|
|
2266
|
+
editorVC.modalTransitionStyle = .crossDissolve
|
|
2267
|
+
}
|
|
1925
2268
|
if sourceFrame != .zero {
|
|
1926
2269
|
self.editorTransitionDelegate = ImageEditorTransitionDelegate()
|
|
1927
2270
|
self.editorTransitionDelegate?.sourceFrame = sourceFrame
|
|
@@ -1961,7 +2304,10 @@ fileprivate extension RNAttachmentKit {
|
|
|
1961
2304
|
// (호출 중인 객체이므로 다음 런루프에서)
|
|
1962
2305
|
DispatchQueue.main.async { self?.editorTransitionDelegate = nil }
|
|
1963
2306
|
}
|
|
1964
|
-
|
|
2307
|
+
// 칸은 열기 애니메이션의 첫 프레임에 비움 (누르자마자 비우면 편집 화면이 준비되는 동안 빈 칸이 번쩍임)
|
|
2308
|
+
self.editorTransitionDelegate?.onPresentationStarted = { [weak currentGallery] in
|
|
2309
|
+
currentGallery?.setHiddenAsset(asset)
|
|
2310
|
+
}
|
|
1965
2311
|
|
|
1966
2312
|
editorVC.transitioningDelegate = self.editorTransitionDelegate
|
|
1967
2313
|
}
|
|
@@ -2085,3 +2431,69 @@ extension RNAttachmentKit: UIViewControllerTransitioningDelegate {
|
|
|
2085
2431
|
return zoomAnimator
|
|
2086
2432
|
}
|
|
2087
2433
|
}
|
|
2434
|
+
|
|
2435
|
+
// MARK: - 전송 처리 상태 (앱에서 동영상 취소)
|
|
2436
|
+
|
|
2437
|
+
/// openGallery 결과 처리 한 번. cancelVideoProcessing(index) → 그 동영상만(또는 전부) 취소
|
|
2438
|
+
private final class MediaProcessingJob {
|
|
2439
|
+
private let lock = NSLock()
|
|
2440
|
+
private var cancelAll = false
|
|
2441
|
+
private var cancelled = Set<Int>()
|
|
2442
|
+
private var currentIndex: Int?
|
|
2443
|
+
private var currentExporter: VideoExporter?
|
|
2444
|
+
|
|
2445
|
+
func cancel(index: Int?) {
|
|
2446
|
+
lock.lock(); defer { lock.unlock() }
|
|
2447
|
+
if let i = index {
|
|
2448
|
+
cancelled.insert(i)
|
|
2449
|
+
if currentIndex == i { currentExporter?.cancel() }
|
|
2450
|
+
} else {
|
|
2451
|
+
cancelAll = true
|
|
2452
|
+
currentExporter?.cancel()
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
func isCancelled(_ i: Int) -> Bool {
|
|
2457
|
+
lock.lock(); defer { lock.unlock() }
|
|
2458
|
+
return cancelAll || cancelled.contains(i)
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
func begin(index: Int, exporter: VideoExporter) {
|
|
2462
|
+
lock.lock(); defer { lock.unlock() }
|
|
2463
|
+
currentIndex = index
|
|
2464
|
+
currentExporter = exporter
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
func end() {
|
|
2468
|
+
lock.lock(); defer { lock.unlock() }
|
|
2469
|
+
currentIndex = nil
|
|
2470
|
+
currentExporter = nil
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
/// onVideoProgress 를 1% 이상 바뀌고 0.1초 지났을 때만 (브릿지가 넘치지 않게). 0 과 1 은 항상
|
|
2475
|
+
private final class ProgressThrottle {
|
|
2476
|
+
private let lock = NSLock()
|
|
2477
|
+
private var last: Double = -1
|
|
2478
|
+
private var lastTime: CFTimeInterval = 0
|
|
2479
|
+
|
|
2480
|
+
func shouldSend(_ p: Double) -> Bool {
|
|
2481
|
+
lock.lock(); defer { lock.unlock() }
|
|
2482
|
+
let now = CACurrentMediaTime()
|
|
2483
|
+
guard p >= 1 || last < 0 || (p - last >= 0.01 && now - lastTime >= 0.1) else { return false }
|
|
2484
|
+
if p >= 1 && last >= 1 { return false }
|
|
2485
|
+
last = p
|
|
2486
|
+
lastTime = now
|
|
2487
|
+
return true
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
/// 스레드 사이에서 읽고 쓰는 Bool
|
|
2492
|
+
private final class AtomicFlag {
|
|
2493
|
+
private let lock = NSLock()
|
|
2494
|
+
private var v = false
|
|
2495
|
+
var value: Bool {
|
|
2496
|
+
get { lock.lock(); defer { lock.unlock() }; return v }
|
|
2497
|
+
set { lock.lock(); v = newValue; lock.unlock() }
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
@@ -8,32 +8,32 @@
|
|
|
8
8
|
<key>BinaryPath</key>
|
|
9
9
|
<string>RNAttachmentKit.framework/RNAttachmentKit</string>
|
|
10
10
|
<key>LibraryIdentifier</key>
|
|
11
|
-
<string>ios-
|
|
11
|
+
<string>ios-arm64</string>
|
|
12
12
|
<key>LibraryPath</key>
|
|
13
13
|
<string>RNAttachmentKit.framework</string>
|
|
14
14
|
<key>SupportedArchitectures</key>
|
|
15
15
|
<array>
|
|
16
16
|
<string>arm64</string>
|
|
17
|
-
<string>x86_64</string>
|
|
18
17
|
</array>
|
|
19
18
|
<key>SupportedPlatform</key>
|
|
20
19
|
<string>ios</string>
|
|
21
|
-
<key>SupportedPlatformVariant</key>
|
|
22
|
-
<string>simulator</string>
|
|
23
20
|
</dict>
|
|
24
21
|
<dict>
|
|
25
22
|
<key>BinaryPath</key>
|
|
26
23
|
<string>RNAttachmentKit.framework/RNAttachmentKit</string>
|
|
27
24
|
<key>LibraryIdentifier</key>
|
|
28
|
-
<string>ios-
|
|
25
|
+
<string>ios-arm64_x86_64-simulator</string>
|
|
29
26
|
<key>LibraryPath</key>
|
|
30
27
|
<string>RNAttachmentKit.framework</string>
|
|
31
28
|
<key>SupportedArchitectures</key>
|
|
32
29
|
<array>
|
|
33
30
|
<string>arm64</string>
|
|
31
|
+
<string>x86_64</string>
|
|
34
32
|
</array>
|
|
35
33
|
<key>SupportedPlatform</key>
|
|
36
34
|
<string>ios</string>
|
|
35
|
+
<key>SupportedPlatformVariant</key>
|
|
36
|
+
<string>simulator</string>
|
|
37
37
|
</dict>
|
|
38
38
|
</array>
|
|
39
39
|
<key>CFBundlePackageType</key>
|
|
Binary file
|
|
@@ -593,6 +593,12 @@ SWIFT_CLASS("_TtC15RNAttachmentKit31RemoteImageViewerViewController")
|
|
|
593
593
|
@interface RemoteImageViewerViewController : UIViewController
|
|
594
594
|
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)coder SWIFT_UNAVAILABLE;
|
|
595
595
|
- (void)viewDidLoad;
|
|
596
|
+
- (void)viewDidAppear:(BOOL)animated;
|
|
597
|
+
- (void)viewWillDisappear:(BOOL)animated;
|
|
598
|
+
@property (nonatomic, readonly) BOOL prefersStatusBarHidden;
|
|
599
|
+
@property (nonatomic, readonly) UIStatusBarAnimation preferredStatusBarUpdateAnimation;
|
|
600
|
+
@property (nonatomic, readonly) BOOL prefersHomeIndicatorAutoHidden;
|
|
601
|
+
- (void)viewDidDisappear:(BOOL)animated;
|
|
596
602
|
- (void)viewDidLayoutSubviews;
|
|
597
603
|
- (nonnull instancetype)initWithNibName:(NSString * _Nullable)nibNameOrNil bundle:(NSBundle * _Nullable)nibBundleOrNil SWIFT_UNAVAILABLE;
|
|
598
604
|
@end
|
|
@@ -602,17 +608,18 @@ SWIFT_CLASS("_TtC15RNAttachmentKit31RemoteImageViewerViewController")
|
|
|
602
608
|
- (BOOL)gestureRecognizer:(UIGestureRecognizer * _Nonnull)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer * _Nonnull)otherGestureRecognizer SWIFT_WARN_UNUSED_RESULT;
|
|
603
609
|
@end
|
|
604
610
|
|
|
605
|
-
@interface RemoteImageViewerViewController (SWIFT_EXTENSION(RNAttachmentKit)) <UIScrollViewDelegate>
|
|
606
|
-
- (void)scrollViewDidScroll:(UIScrollView * _Nonnull)scrollView;
|
|
607
|
-
- (void)scrollViewDidEndDecelerating:(UIScrollView * _Nonnull)scrollView;
|
|
608
|
-
@end
|
|
609
|
-
|
|
610
611
|
@interface RemoteImageViewerViewController (SWIFT_EXTENSION(RNAttachmentKit)) <UICollectionViewDataSource, UICollectionViewDelegate>
|
|
611
612
|
- (NSInteger)collectionView:(UICollectionView * _Nonnull)collectionView numberOfItemsInSection:(NSInteger)section SWIFT_WARN_UNUSED_RESULT;
|
|
612
613
|
- (UICollectionViewCell * _Nonnull)collectionView:(UICollectionView * _Nonnull)collectionView cellForItemAtIndexPath:(NSIndexPath * _Nonnull)indexPath SWIFT_WARN_UNUSED_RESULT;
|
|
613
614
|
- (void)collectionView:(UICollectionView * _Nonnull)collectionView didSelectItemAtIndexPath:(NSIndexPath * _Nonnull)indexPath;
|
|
614
615
|
@end
|
|
615
616
|
|
|
617
|
+
@interface RemoteImageViewerViewController (SWIFT_EXTENSION(RNAttachmentKit)) <UIScrollViewDelegate>
|
|
618
|
+
- (void)scrollViewDidScroll:(UIScrollView * _Nonnull)scrollView;
|
|
619
|
+
- (void)scrollViewDidEndDragging:(UIScrollView * _Nonnull)scrollView willDecelerate:(BOOL)decelerate;
|
|
620
|
+
- (void)scrollViewDidEndDecelerating:(UIScrollView * _Nonnull)scrollView;
|
|
621
|
+
@end
|
|
622
|
+
|
|
616
623
|
@class UICollectionViewLayoutAttributes;
|
|
617
624
|
SWIFT_CLASS("_TtC15RNAttachmentKit21TelegramGalleryLayout")
|
|
618
625
|
@interface TelegramGalleryLayout : UICollectionViewLayout
|
|
@@ -626,6 +633,15 @@ SWIFT_CLASS("_TtC15RNAttachmentKit21TelegramGalleryLayout")
|
|
|
626
633
|
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)coder OBJC_DESIGNATED_INITIALIZER;
|
|
627
634
|
@end
|
|
628
635
|
|
|
636
|
+
/// 동영상 변환 중 화면 전체를 덮는 진행 표시 (원형 진행률 + “1/3” + 취소). 갤러리가 닫힌 뒤에도 보여야 해서 키 윈도우에 얹음
|
|
637
|
+
SWIFT_CLASS("_TtC15RNAttachmentKit14VideoExportHUD")
|
|
638
|
+
@interface VideoExportHUD : UIView
|
|
639
|
+
- (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)coder SWIFT_UNAVAILABLE;
|
|
640
|
+
- (void)layoutSubviews;
|
|
641
|
+
- (void)willMoveToSuperview:(UIView * _Nullable)newSuperview;
|
|
642
|
+
- (nonnull instancetype)initWithFrame:(CGRect)frame SWIFT_UNAVAILABLE;
|
|
643
|
+
@end
|
|
644
|
+
|
|
629
645
|
@protocol UIViewControllerContextTransitioning;
|
|
630
646
|
SWIFT_CLASS("_TtC15RNAttachmentKit22ZoomTransitionAnimator")
|
|
631
647
|
@interface ZoomTransitionAnimator : NSObject <UIViewControllerAnimatedTransitioning>
|