react-native-attachment-kit 1.0.5 → 1.0.8
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 +18 -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 +447 -9
- 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 +33440 -7566
- 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 +33440 -7566
- 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 +33440 -7566
- 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 +2 -2
- 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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import UIKit
|
|
9
9
|
import Photos
|
|
10
|
+
import AVFoundation
|
|
10
11
|
import ImageIO
|
|
11
12
|
import CoreLocation
|
|
12
13
|
import RNAttachmentKit
|
|
@@ -49,6 +50,13 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
49
50
|
private var editorTransitionDelegate: ImageEditorTransitionDelegate?
|
|
50
51
|
/// 열려 있는 미디어 갤러리 (galleryId → 화면)
|
|
51
52
|
fileprivate var mediaGalleries: [String: MediaGalleryViewController] = [:]
|
|
53
|
+
/// 진행 중인 전송 처리 (processingId → 작업, 메인 스레드에서만). 보내는 중에 갤러리를 다시 열어 또 보낼 수 있어 여러 개일 수 있음
|
|
54
|
+
private var processingJobs: [String: MediaProcessingJob] = [:]
|
|
55
|
+
/// 가장 최근에 시작한 처리 (cancelVideoProcessing 에 processingId 가 없을 때)
|
|
56
|
+
private var lastProcessingId: String?
|
|
57
|
+
/// 동영상 변환 진행 HUD 를 띄울지 — video.showProgressHUD, 없으면 asyncProcessing 이 아닐 때만
|
|
58
|
+
/// (앱이 onProcessingStart / onVideoProgress / onImageProcessed 로 말풍선에 진행 상황을 그리는 구조면 HUD 없이)
|
|
59
|
+
private var galleryShowsVideoHUD = true
|
|
52
60
|
|
|
53
61
|
// MARK: - Module Setup
|
|
54
62
|
|
|
@@ -66,7 +74,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
66
74
|
override func supportedEvents() -> [String]! {
|
|
67
75
|
return ["onSelectionChange", "onImageProcessed", "onPageSelected", "onViewerWillClose", "onImageDeleted",
|
|
68
76
|
"onGalleryItemDeleted", "onGalleryNeedPage", "onGalleryClosed", "onGalleryGridSizeChanged",
|
|
69
|
-
"onLocationSelected", "onFilesSelected"
|
|
77
|
+
"onLocationSelected", "onFilesSelected", "onAIUsage", "onAIChargeRequired", "onVideoProgress",
|
|
78
|
+
"onMapViewerWillClose", "onProcessingStart"]
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
// MARK: - Public Methods
|
|
@@ -270,6 +279,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
270
279
|
vc.sourceImageIsMarkerless = markerless && image != nil
|
|
271
280
|
vc.sourceRectProvider = { [weak self] in self?.mapViewerSourceRect }
|
|
272
281
|
}
|
|
282
|
+
vc.onWillClose = { [weak self] dur in
|
|
283
|
+
self?.sendEvent(withName: "onMapViewerWillClose", body: ["durationMs": Int(dur * 1000)])
|
|
284
|
+
}
|
|
273
285
|
vc.onClosed = { [weak self] in
|
|
274
286
|
self?.mapViewerSourceRect = nil
|
|
275
287
|
resolve(nil)
|
|
@@ -365,6 +377,20 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
365
377
|
}
|
|
366
378
|
}
|
|
367
379
|
|
|
380
|
+
/// AI 기능 설정·잔액 (넘긴 값만 바뀜). 사용·충전 요청은 onAIUsage / onAIChargeRequired 이벤트로 알림
|
|
381
|
+
@objc
|
|
382
|
+
func setAIConfig(_ config: NSDictionary,
|
|
383
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
384
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
385
|
+
let dict = config as? [String: Any] ?? [:]
|
|
386
|
+
DispatchQueue.main.async {
|
|
387
|
+
AIBilling.apply(dict)
|
|
388
|
+
AIBilling.onUsage = { [weak self] body in self?.sendEvent(withName: "onAIUsage", body: body) }
|
|
389
|
+
AIBilling.onChargeRequired = { [weak self] body in self?.sendEvent(withName: "onAIChargeRequired", body: body) }
|
|
390
|
+
resolve(nil)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
368
394
|
@objc
|
|
369
395
|
func clearReverseGeocodeCache(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
370
396
|
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
@@ -393,6 +419,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
393
419
|
let placeholderImages = options["placeholderImages"] as? [String]
|
|
394
420
|
// cacheFiles: images 와 같은 순서의 로컬 캐시 파일 경로 (null 항목은 NSNull 로 옴 → 빈 문자열)
|
|
395
421
|
let cacheFiles: [String]? = (options["cacheFiles"] as? [Any])?.map { ($0 as? String) ?? "" }
|
|
422
|
+
// videos: images 와 같은 순서. 항목이 있으면 그 페이지는 동영상 (images[i] 는 포스터)
|
|
423
|
+
let viewerVideos: [ViewerVideoItem?] = (options["videos"] as? [Any])?.map { ViewerVideoItem(dictionary: $0) } ?? []
|
|
396
424
|
let themeColorHex = options["themeColor"] as? String
|
|
397
425
|
let languageCode = options["languageCode"] as? String ?? "en"
|
|
398
426
|
let viewerTitle = options["title"] as? String
|
|
@@ -476,6 +504,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
476
504
|
viewerVC.viewerTitle = viewerTitle
|
|
477
505
|
viewerVC.placeholderImages = placeholderImages
|
|
478
506
|
viewerVC.cacheFiles = cacheFiles
|
|
507
|
+
viewerVC.videos = viewerVideos
|
|
479
508
|
if let hex = themeColorHex, let color = UIColor(hexString: hex) {
|
|
480
509
|
viewerVC.themeColor = color
|
|
481
510
|
}
|
|
@@ -941,6 +970,14 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
941
970
|
galleryVC.showsAttachMenu = options["showAttachMenu"] as? Bool ?? false
|
|
942
971
|
// 위치 보내기 때 지도 캡처 이미지 (기본 true). false 면 위치 정보만 — 받는 쪽이 renderLocationThumbnail 로 그림
|
|
943
972
|
galleryVC.capturesLocationImage = options["captureLocationImage"] as? Bool ?? true
|
|
973
|
+
// 동영상: mediaType "photo"(기본) | "video" | "all" + video 옵션 (지시서 3-1)
|
|
974
|
+
let mediaTypeOption = (options["mediaType"] as? String)?.lowercased() ?? "photo"
|
|
975
|
+
galleryVC.mediaType = ["photo", "video", "all"].contains(mediaTypeOption) ? mediaTypeOption : "photo"
|
|
976
|
+
let videoOptions = VideoExportOptions(dictionary: options["video"] as? [String: Any])
|
|
977
|
+
let asyncProcessing = options["asyncProcessing"] as? Bool ?? false
|
|
978
|
+
self.galleryShowsVideoHUD = ((options["video"] as? [String: Any])?["showProgressHUD"] as? Bool) ?? !asyncProcessing
|
|
979
|
+
galleryVC.maxVideoDurationSec = videoOptions.maxDurationSec
|
|
980
|
+
galleryVC.videoExportOptions = videoOptions
|
|
944
981
|
// 위치 탭에서 보내기 → onLocationSelected 이벤트로 결과를 넘기고, openGallery 는 빈 배열로 끝냄
|
|
945
982
|
galleryVC.onLocationSelected = { [weak self, weak galleryVC] location in
|
|
946
983
|
self?.currentGalleryVC = nil
|
|
@@ -965,6 +1002,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
965
1002
|
let galleryMaxWidth = galleryVC.maxWidth
|
|
966
1003
|
let galleryMaxHeight = galleryVC.maxHeight
|
|
967
1004
|
let galleryFormat = galleryVC.outputFormat // "webp" | "jpg" | "png"
|
|
1005
|
+
let galleryAccent = galleryVC.parsedThemeColor
|
|
1006
|
+
let galleryLanguage = galleryVC.languageCode
|
|
968
1007
|
galleryVC.onImagesSelected = { [weak self, weak galleryVC] selectedImages in
|
|
969
1008
|
guard let self = self else { resolve([]); return }
|
|
970
1009
|
|
|
@@ -987,16 +1026,126 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
987
1026
|
// 처리한 이미지는 바로 놓아 주도록 (전체 UIImage 배열을 끝까지 들고 있지 않게) 옵셔널 배열로 옮김
|
|
988
1027
|
var pending: [(PHAsset?, UIImage)?] = selectedImages.map { $0 }
|
|
989
1028
|
|
|
1029
|
+
// 동영상이 섞여 있으면 변환 진행 표시 (갤러리가 닫힌 뒤에도 보이게 키 윈도우에) + 취소
|
|
1030
|
+
let hasVideo = selectedImages.contains { $0.0?.mediaType == .video }
|
|
1031
|
+
// 편집 화면에서 고른 품질·음소거 (없으면 기본 품질)
|
|
1032
|
+
let videoStates = galleryVC?.videoEditStates ?? [:]
|
|
1033
|
+
var hud: VideoExportHUD?
|
|
1034
|
+
var currentExporter: VideoExporter?
|
|
1035
|
+
var cancelledAll = false
|
|
1036
|
+
let exporterLock = NSLock()
|
|
1037
|
+
// 앱이 동영상 하나(또는 전부)를 취소할 수 있게 (cancelVideoProcessing)
|
|
1038
|
+
let job = MediaProcessingJob()
|
|
1039
|
+
let processingId = UUID().uuidString
|
|
1040
|
+
self.processingJobs[processingId] = job
|
|
1041
|
+
self.lastProcessingId = processingId
|
|
1042
|
+
// 처리 시작 이벤트용 — 고른 순서의 원본(사진은 편집 결과) 크기·포스터
|
|
1043
|
+
let startItems = selectedImages.map { ($0.0, $0.1) }
|
|
1044
|
+
if hasVideo && self.galleryShowsVideoHUD {
|
|
1045
|
+
hud = VideoExportHUD.show(accent: galleryAccent, languageCode: galleryLanguage)
|
|
1046
|
+
hud?.onCancel = {
|
|
1047
|
+
exporterLock.lock(); cancelledAll = true; currentExporter?.cancel(); exporterLock.unlock()
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
990
1051
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
1052
|
+
// ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
|
|
1053
|
+
self.sendEvent(withName: "onProcessingStart", body: [
|
|
1054
|
+
"processingId": processingId,
|
|
1055
|
+
"total": total,
|
|
1056
|
+
"items": startItems.enumerated().map { i, pair in
|
|
1057
|
+
self.processingStartItem(index: i, asset: pair.0, image: pair.1, videoStates: videoStates,
|
|
1058
|
+
videoOptions: videoOptions, maxWidth: maxWidth, maxHeight: maxHeight,
|
|
1059
|
+
tempDir: tempDir)
|
|
1060
|
+
},
|
|
1061
|
+
])
|
|
991
1062
|
for index in 0..<total {
|
|
992
1063
|
guard let pair = pending[index] else { continue }
|
|
993
1064
|
pending[index] = nil
|
|
1065
|
+
exporterLock.lock(); let stop = cancelledAll; exporterLock.unlock()
|
|
1066
|
+
if stop { break }
|
|
994
1067
|
autoreleasepool {
|
|
995
1068
|
let (asset, image) = pair
|
|
996
1069
|
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
997
1070
|
let originalSize = image.size
|
|
998
1071
|
|
|
999
|
-
if let phAsset = asset {
|
|
1072
|
+
if let phAsset = asset, phAsset.mediaType == .video {
|
|
1073
|
+
// ✅ 동영상: VideoExporter 로 전송용 mp4 (+ 썸네일·메타). image 는 포스터(목록용)
|
|
1074
|
+
let exporter = VideoExporter()
|
|
1075
|
+
exporterLock.lock(); currentExporter = exporter; exporterLock.unlock()
|
|
1076
|
+
let base = "turbo_video_\(timestamp)_\(index)"
|
|
1077
|
+
var result: [String: Any] = [
|
|
1078
|
+
"originalUri": "ph://\(phAsset.localIdentifier)",
|
|
1079
|
+
"originalWidth": phAsset.pixelWidth,
|
|
1080
|
+
"originalHeight": phAsset.pixelHeight,
|
|
1081
|
+
"mediaType": "video",
|
|
1082
|
+
"type": "video/mp4",
|
|
1083
|
+
"duration": phAsset.duration,
|
|
1084
|
+
]
|
|
1085
|
+
do {
|
|
1086
|
+
// 앱이 이미 취소한 동영상은 건너뜀 (결과에 cancelled 로)
|
|
1087
|
+
if job.isCancelled(index) { throw VideoExporter.ExportError.cancelled }
|
|
1088
|
+
job.begin(index: index, exporter: exporter)
|
|
1089
|
+
let state = videoStates[phAsset.localIdentifier] ?? VideoEditState.defaultState(for: phAsset, options: videoOptions)
|
|
1090
|
+
let throttle = ProgressThrottle()
|
|
1091
|
+
let onProgress: (Double) -> Void = { p in
|
|
1092
|
+
// 프레임마다 메인으로 보내지 않고 1%·0.1초 단위로만 (HUD 표시는 정수 % 라 같음)
|
|
1093
|
+
guard throttle.shouldSend(p) else { return }
|
|
1094
|
+
DispatchQueue.main.async { hud?.update(progress: p, index: index, total: total) }
|
|
1095
|
+
self.sendEvent(withName: "onVideoProgress", body: ["index": index, "total": total, "progress": p, "processingId": processingId])
|
|
1096
|
+
}
|
|
1097
|
+
// 편집 화면에서 미리 만들어 둔 파일이 있으면(또는 만드는 중이면 끝나고) 그대로 씀
|
|
1098
|
+
let r: VideoExportResult
|
|
1099
|
+
// 기다리는 동안 앱(X)이나 HUD 가 취소하면 바로 멈춤 (전엔 미리 만들기가 끝날 때까지 못 빠져나왔음)
|
|
1100
|
+
let stop = { () -> Bool in
|
|
1101
|
+
exporterLock.lock(); let all = cancelledAll; exporterLock.unlock()
|
|
1102
|
+
return all || job.isCancelled(index)
|
|
1103
|
+
}
|
|
1104
|
+
if let ready = VideoPrepareManager.shared.takeReady(asset: phAsset, state: state, progress: onProgress, isCancelled: stop) {
|
|
1105
|
+
r = ready; onProgress(1)
|
|
1106
|
+
} else {
|
|
1107
|
+
if stop() { throw VideoExporter.ExportError.cancelled }
|
|
1108
|
+
r = try exporter.export(asset: phAsset, state: state,
|
|
1109
|
+
options: videoOptions, outputDir: URL(fileURLWithPath: tempDir), baseName: base, progress: onProgress)
|
|
1110
|
+
}
|
|
1111
|
+
// 미리 만든 파일을 기다리는 동안 취소됐으면 결과를 쓰지 않음
|
|
1112
|
+
if job.isCancelled(index) { throw VideoExporter.ExportError.cancelled }
|
|
1113
|
+
result["uri"] = "file://" + r.url.path
|
|
1114
|
+
result["width"] = r.width
|
|
1115
|
+
result["height"] = r.height
|
|
1116
|
+
result["duration"] = r.duration
|
|
1117
|
+
result["fileSize"] = r.fileSize
|
|
1118
|
+
result["fileName"] = r.url.lastPathComponent
|
|
1119
|
+
result["fileExtension"] = "mp4"
|
|
1120
|
+
result["videoCodec"] = r.codec
|
|
1121
|
+
result["videoBitrate"] = r.bitrate
|
|
1122
|
+
result["edited"] = !r.passthrough
|
|
1123
|
+
if let t = r.thumbnailURL {
|
|
1124
|
+
result["thumbnailUri"] = "file://" + t.path
|
|
1125
|
+
result["thumbnailWidth"] = r.thumbnailWidth
|
|
1126
|
+
result["thumbnailHeight"] = r.thumbnailHeight
|
|
1127
|
+
}
|
|
1128
|
+
} catch VideoExporter.ExportError.cancelled where job.isCancelled(index) {
|
|
1129
|
+
// 앱이 이 동영상만 취소 → 나머지는 계속
|
|
1130
|
+
result["error"] = "CANCELLED"
|
|
1131
|
+
result["cancelled"] = true
|
|
1132
|
+
} catch VideoExporter.ExportError.cancelled {
|
|
1133
|
+
// HUD 의 취소 → 전체 취소 (Promise reject)
|
|
1134
|
+
job.end()
|
|
1135
|
+
exporterLock.lock(); cancelledAll = true; exporterLock.unlock()
|
|
1136
|
+
return
|
|
1137
|
+
} catch {
|
|
1138
|
+
result["error"] = error.localizedDescription
|
|
1139
|
+
}
|
|
1140
|
+
job.end()
|
|
1141
|
+
exporterLock.lock(); currentExporter = nil; exporterLock.unlock()
|
|
1142
|
+
|
|
1143
|
+
resultsLock.lock()
|
|
1144
|
+
results.append(result)
|
|
1145
|
+
let currentResult = result
|
|
1146
|
+
resultsLock.unlock()
|
|
1147
|
+
self.sendEvent(withName: "onImageProcessed", body: ["index": index, "total": total, "image": currentResult, "processingId": processingId])
|
|
1148
|
+
} else if let phAsset = asset {
|
|
1000
1149
|
// ✅ 1순위: PHAsset
|
|
1001
1150
|
let originalUri = "ph://\(phAsset.localIdentifier)"
|
|
1002
1151
|
|
|
@@ -1036,7 +1185,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1036
1185
|
self.sendEvent(withName: "onImageProcessed", body: [
|
|
1037
1186
|
"index": index,
|
|
1038
1187
|
"total": total,
|
|
1039
|
-
"image": currentResult
|
|
1188
|
+
"image": currentResult,
|
|
1189
|
+
"processingId": processingId
|
|
1040
1190
|
])
|
|
1041
1191
|
} else {
|
|
1042
1192
|
// ✅ 2순위: 카메라 촬영본
|
|
@@ -1086,17 +1236,30 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1086
1236
|
self.sendEvent(withName: "onImageProcessed", body: [
|
|
1087
1237
|
"index": index,
|
|
1088
1238
|
"total": total,
|
|
1089
|
-
"image": currentResult
|
|
1239
|
+
"image": currentResult,
|
|
1240
|
+
"processingId": processingId
|
|
1090
1241
|
])
|
|
1091
1242
|
}
|
|
1092
1243
|
} // end autoreleasepool
|
|
1093
1244
|
} // end for
|
|
1094
1245
|
|
|
1095
1246
|
DispatchQueue.main.async {
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1247
|
+
hud?.dismiss()
|
|
1248
|
+
// 처리하는 동안 새 갤러리가 열렸을 수 있음 → 내 갤러리일 때만 비움
|
|
1249
|
+
if self.currentGalleryVC == nil || self.currentGalleryVC === galleryVC { self.currentGalleryVC = nil }
|
|
1250
|
+
self.processingJobs[processingId] = nil
|
|
1251
|
+
exporterLock.lock(); let wasCancelled = cancelledAll; exporterLock.unlock()
|
|
1252
|
+
if wasCancelled {
|
|
1253
|
+
// 변환 취소: 이미 만든 파일은 버림
|
|
1254
|
+
results.compactMap { ($0["uri"] as? String)?.replacingOccurrences(of: "file://", with: "") }
|
|
1255
|
+
.forEach { try? FileManager.default.removeItem(atPath: $0) }
|
|
1256
|
+
reject("CANCELLED", "User cancelled video conversion", nil)
|
|
1257
|
+
} else {
|
|
1258
|
+
resolve(results)
|
|
1259
|
+
}
|
|
1260
|
+
// 결과를 넘긴 뒤: 갤러리의 PHCachingImageManager 캐시·콜백 해제 (닫힘 애니메이션에는 영향 없음).
|
|
1261
|
+
// 새 갤러리가 열려 있으면 그 화면이 미리 인코딩 중인 파일은 건드리지 않음
|
|
1262
|
+
galleryVC?.releaseAfterReturn(discardPreparedVideos: self.currentGalleryVC == nil)
|
|
1100
1263
|
}
|
|
1101
1264
|
}
|
|
1102
1265
|
}
|
|
@@ -1150,6 +1313,100 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1150
1313
|
}
|
|
1151
1314
|
}
|
|
1152
1315
|
|
|
1316
|
+
/// 동영상 변환 취소 (HUD 없이 앱이 말풍선에서 취소할 때). index 생략(-1) = 남은 동영상 전부.
|
|
1317
|
+
/// 취소된 동영상은 onImageProcessed / 결과에 { error: "CANCELLED", cancelled: true } 로 오고, 나머지는 계속 처리
|
|
1318
|
+
/// processingId 생략("") = 가장 최근 처리
|
|
1319
|
+
@objc(cancelVideoProcessing:processingId:resolve:reject:)
|
|
1320
|
+
func cancelVideoProcessing(_ index: NSNumber, processingId: NSString, resolve: @escaping RCTPromiseResolveBlock,
|
|
1321
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
1322
|
+
DispatchQueue.main.async {
|
|
1323
|
+
let id = processingId.length > 0 ? processingId as String : self.lastProcessingId
|
|
1324
|
+
guard let id = id, let job = self.processingJobs[id] else { resolve(false); return }
|
|
1325
|
+
let i = index.intValue
|
|
1326
|
+
job.cancel(index: i >= 0 ? i : nil)
|
|
1327
|
+
resolve(true)
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/// onProcessingStart 의 한 자리: 사진은 예상 결과 크기, 동영상은 예상 결과 크기·길이(구간 반영)·포스터(크롭·그리기 반영 jpg)
|
|
1332
|
+
private func processingStartItem(index: Int, asset: PHAsset?, image: UIImage?, videoStates: [String: VideoEditState],
|
|
1333
|
+
videoOptions: VideoExportOptions, maxWidth: Int, maxHeight: Int, tempDir: String) -> [String: Any] {
|
|
1334
|
+
if let a = asset, a.mediaType == .video {
|
|
1335
|
+
let state = videoStates[a.localIdentifier] ?? VideoEditState.defaultState(for: a, options: videoOptions)
|
|
1336
|
+
let d = CGSize(width: a.pixelWidth, height: a.pixelHeight)
|
|
1337
|
+
let size = VideoExporter.predictedOutputSize(displaySize: d, state: state, options: videoOptions, sourceDuration: a.duration)
|
|
1338
|
+
var item: [String: Any] = [
|
|
1339
|
+
"index": index, "mediaType": "video", "originalUri": "ph://\(a.localIdentifier)",
|
|
1340
|
+
"width": Int(size.width), "height": Int(size.height),
|
|
1341
|
+
"duration": state.outputDuration(sourceDuration: a.duration),
|
|
1342
|
+
]
|
|
1343
|
+
// 앞을 잘랐으면 포스터는 잘린 구간의 첫 프레임 (원본 첫 프레임은 결과 영상에 없음 — Android 와 같음)
|
|
1344
|
+
let trimmedStart = state.trimRange(duration: a.duration).map { CMTimeGetSeconds($0.start) } ?? 0
|
|
1345
|
+
let base = trimmedStart > 0.01 ? (Self.videoFrame(a, at: trimmedStart) ?? image) : image
|
|
1346
|
+
if let poster = base {
|
|
1347
|
+
let edited = state.applyEdits(to: poster)
|
|
1348
|
+
if let encoded = VideoExporter.encodePoster(edited, format: videoOptions.posterFormat, quality: 0.8) {
|
|
1349
|
+
let path = (tempDir as NSString).appendingPathComponent("turbo_video_poster_\(Int(Date().timeIntervalSince1970 * 1000))_\(index).\(encoded.fileExtension)")
|
|
1350
|
+
if (try? encoded.data.write(to: URL(fileURLWithPath: path))) != nil {
|
|
1351
|
+
item["thumbnailUri"] = "file://" + path
|
|
1352
|
+
item["thumbnailWidth"] = Int(edited.size.width * edited.scale)
|
|
1353
|
+
item["thumbnailHeight"] = Int(edited.size.height * edited.scale)
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return item
|
|
1358
|
+
}
|
|
1359
|
+
// 사진: 리사이즈 규칙(resizeImage)과 같은 예상 크기 (resizeImage 처럼 image.size 기준)
|
|
1360
|
+
var w = image?.size.width ?? CGFloat(asset?.pixelWidth ?? 0)
|
|
1361
|
+
var h = image?.size.height ?? CGFloat(asset?.pixelHeight ?? 0)
|
|
1362
|
+
if w > 0, h > 0 {
|
|
1363
|
+
var ratio: CGFloat = 1
|
|
1364
|
+
if maxWidth > 0 && maxHeight > 0 { ratio = min(CGFloat(maxWidth) / w, CGFloat(maxHeight) / h, 1) }
|
|
1365
|
+
else if maxWidth > 0 { ratio = min(CGFloat(maxWidth) / w, 1) }
|
|
1366
|
+
else if maxHeight > 0 { ratio = min(CGFloat(maxHeight) / h, 1) }
|
|
1367
|
+
w *= ratio; h *= ratio
|
|
1368
|
+
}
|
|
1369
|
+
var item: [String: Any] = ["index": index, "mediaType": "image", "width": Int(w.rounded()), "height": Int(h.rounded())]
|
|
1370
|
+
if let a = asset { item["originalUri"] = "ph://\(a.localIdentifier)" }
|
|
1371
|
+
return item
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/// 동영상 포스터 (긴 변 512, 동기) — 처리 시작 이벤트용
|
|
1375
|
+
/// 동영상의 [seconds] 프레임 (긴 변 ≤ 1024, 회전 반영). 백그라운드 스레드에서만 — 원본을 받는 동안 최대 3초 기다림
|
|
1376
|
+
private static func videoFrame(_ asset: PHAsset, at seconds: Double) -> UIImage? {
|
|
1377
|
+
let o = PHVideoRequestOptions()
|
|
1378
|
+
o.isNetworkAccessAllowed = true
|
|
1379
|
+
o.deliveryMode = .automatic
|
|
1380
|
+
var avAsset: AVAsset?
|
|
1381
|
+
let done = DispatchSemaphore(value: 0)
|
|
1382
|
+
PHImageManager.default().requestAVAsset(forVideo: asset, options: o) { a, _, _ in
|
|
1383
|
+
avAsset = a
|
|
1384
|
+
done.signal()
|
|
1385
|
+
}
|
|
1386
|
+
guard done.wait(timeout: .now() + 3) == .success, let av = avAsset else { return nil }
|
|
1387
|
+
let gen = AVAssetImageGenerator(asset: av)
|
|
1388
|
+
gen.appliesPreferredTrackTransform = true
|
|
1389
|
+
gen.maximumSize = CGSize(width: 1024, height: 1024)
|
|
1390
|
+
// 잘린 구간 밖(앞) 프레임이 나오지 않게 — 뒤로만 조금 허용
|
|
1391
|
+
gen.requestedTimeToleranceBefore = .zero
|
|
1392
|
+
gen.requestedTimeToleranceAfter = CMTime(seconds: 0.1, preferredTimescale: 600)
|
|
1393
|
+
guard let cg = try? gen.copyCGImage(at: CMTime(seconds: seconds, preferredTimescale: 600), actualTime: nil) else { return nil }
|
|
1394
|
+
return UIImage(cgImage: cg)
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
private static func requestPoster(_ asset: PHAsset) -> UIImage? {
|
|
1398
|
+
let o = PHImageRequestOptions()
|
|
1399
|
+
o.isSynchronous = true
|
|
1400
|
+
o.deliveryMode = .highQualityFormat
|
|
1401
|
+
o.resizeMode = .fast
|
|
1402
|
+
o.isNetworkAccessAllowed = true
|
|
1403
|
+
var out: UIImage?
|
|
1404
|
+
PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 512, height: 512), contentMode: .aspectFit, options: o) { img, _ in
|
|
1405
|
+
out = img
|
|
1406
|
+
}
|
|
1407
|
+
return out
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1153
1410
|
@objc
|
|
1154
1411
|
func closeGallery(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
1155
1412
|
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
@@ -1457,9 +1714,105 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1457
1714
|
}
|
|
1458
1715
|
|
|
1459
1716
|
// MARK: - Editor Result Processing
|
|
1717
|
+
|
|
1718
|
+
/// 동영상 1장을 전송용 mp4 로 만들어 resolve (갤러리 다중 전송 경로의 동영상 분기와 같은 필드)
|
|
1719
|
+
private func exportSingleVideo(_ phAsset: PHAsset, galleryVC: GalleryViewController?, tempDir: String,
|
|
1720
|
+
resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
1721
|
+
let videoOptions = galleryVC?.videoExportOptions ?? VideoExportOptions()
|
|
1722
|
+
let state = galleryVC?.videoEditStates[phAsset.localIdentifier] ?? VideoEditState.defaultState(for: phAsset, options: videoOptions)
|
|
1723
|
+
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
|
|
1724
|
+
let exporter = VideoExporter()
|
|
1725
|
+
let hudCancelled = AtomicFlag()
|
|
1726
|
+
hud?.onCancel = { hudCancelled.value = true; exporter.cancel() }
|
|
1727
|
+
let job = MediaProcessingJob()
|
|
1728
|
+
let processingId = UUID().uuidString
|
|
1729
|
+
processingJobs[processingId] = job
|
|
1730
|
+
lastProcessingId = processingId
|
|
1731
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
1732
|
+
let poster = Self.requestPoster(phAsset)
|
|
1733
|
+
self.sendEvent(withName: "onProcessingStart", body: [
|
|
1734
|
+
"processingId": processingId,
|
|
1735
|
+
"total": 1,
|
|
1736
|
+
"items": [self.processingStartItem(index: 0, asset: phAsset, image: poster, videoStates: [phAsset.localIdentifier: state],
|
|
1737
|
+
videoOptions: videoOptions, maxWidth: 0, maxHeight: 0, tempDir: tempDir)],
|
|
1738
|
+
])
|
|
1739
|
+
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
1740
|
+
var result: [String: Any] = [
|
|
1741
|
+
"originalUri": "ph://\(phAsset.localIdentifier)",
|
|
1742
|
+
"originalWidth": phAsset.pixelWidth,
|
|
1743
|
+
"originalHeight": phAsset.pixelHeight,
|
|
1744
|
+
"mediaType": "video",
|
|
1745
|
+
"type": "video/mp4",
|
|
1746
|
+
"duration": phAsset.duration,
|
|
1747
|
+
]
|
|
1748
|
+
do {
|
|
1749
|
+
job.begin(index: 0, exporter: exporter)
|
|
1750
|
+
let throttle = ProgressThrottle()
|
|
1751
|
+
let onProgress: (Double) -> Void = { p in
|
|
1752
|
+
guard throttle.shouldSend(p) else { return }
|
|
1753
|
+
DispatchQueue.main.async { hud?.update(progress: p, index: 0, total: 1) }
|
|
1754
|
+
self.sendEvent(withName: "onVideoProgress", body: ["index": 0, "total": 1, "progress": p, "processingId": processingId])
|
|
1755
|
+
}
|
|
1756
|
+
let r: VideoExportResult
|
|
1757
|
+
// HUD 취소는 exporter.cancel() — 기다리는 동안엔 job 으로 알 수 없으므로 HUD 취소도 job 에 기록
|
|
1758
|
+
if let ready = VideoPrepareManager.shared.takeReady(asset: phAsset, state: state, progress: onProgress,
|
|
1759
|
+
isCancelled: { job.isCancelled(0) || hudCancelled.value }) {
|
|
1760
|
+
r = ready; onProgress(1)
|
|
1761
|
+
} else {
|
|
1762
|
+
if job.isCancelled(0) || hudCancelled.value { throw VideoExporter.ExportError.cancelled }
|
|
1763
|
+
r = try exporter.export(asset: phAsset, state: state, options: videoOptions,
|
|
1764
|
+
outputDir: URL(fileURLWithPath: tempDir), baseName: "turbo_video_\(timestamp)_0", progress: onProgress)
|
|
1765
|
+
}
|
|
1766
|
+
result["uri"] = "file://" + r.url.path
|
|
1767
|
+
result["width"] = r.width
|
|
1768
|
+
result["height"] = r.height
|
|
1769
|
+
result["duration"] = r.duration
|
|
1770
|
+
result["fileSize"] = r.fileSize
|
|
1771
|
+
result["fileName"] = r.url.lastPathComponent
|
|
1772
|
+
result["fileExtension"] = "mp4"
|
|
1773
|
+
result["videoCodec"] = r.codec
|
|
1774
|
+
result["videoBitrate"] = r.bitrate
|
|
1775
|
+
result["edited"] = !r.passthrough
|
|
1776
|
+
if let t = r.thumbnailURL {
|
|
1777
|
+
result["thumbnailUri"] = "file://" + t.path
|
|
1778
|
+
result["thumbnailWidth"] = r.thumbnailWidth
|
|
1779
|
+
result["thumbnailHeight"] = r.thumbnailHeight
|
|
1780
|
+
}
|
|
1781
|
+
if job.isCancelled(0) { throw VideoExporter.ExportError.cancelled }
|
|
1782
|
+
} catch VideoExporter.ExportError.cancelled where job.isCancelled(0) {
|
|
1783
|
+
// 앱이 취소 (cancelVideoProcessing) — 이벤트·결과에 cancelled 로
|
|
1784
|
+
result["error"] = "CANCELLED"
|
|
1785
|
+
result["cancelled"] = true
|
|
1786
|
+
} catch VideoExporter.ExportError.cancelled {
|
|
1787
|
+
job.end()
|
|
1788
|
+
DispatchQueue.main.async {
|
|
1789
|
+
self.processingJobs[processingId] = nil
|
|
1790
|
+
hud?.dismiss(); reject("CANCELLED", "Video export cancelled", nil)
|
|
1791
|
+
}
|
|
1792
|
+
return
|
|
1793
|
+
} catch {
|
|
1794
|
+
result["error"] = error.localizedDescription
|
|
1795
|
+
}
|
|
1796
|
+
job.end()
|
|
1797
|
+
self.sendEvent(withName: "onImageProcessed", body: ["index": 0, "total": 1, "image": result, "processingId": processingId])
|
|
1798
|
+
DispatchQueue.main.async {
|
|
1799
|
+
hud?.dismiss()
|
|
1800
|
+
if self.currentGalleryVC == nil || self.currentGalleryVC === galleryVC { self.currentGalleryVC = nil }
|
|
1801
|
+
self.processingJobs[processingId] = nil
|
|
1802
|
+
galleryVC?.releaseAfterReturn(discardPreparedVideos: self.currentGalleryVC == nil)
|
|
1803
|
+
resolve([result])
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1460
1808
|
|
|
1461
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) {
|
|
1462
1810
|
let tempDir = prepareCacheDirectory()
|
|
1811
|
+
// 동영상 1장 편집 → 전송: 편집 화면에서 고른 품질·음소거로 VideoExporter (사진 경로와 같은 결과 형태)
|
|
1812
|
+
if let phAsset = asset, phAsset.mediaType == .video {
|
|
1813
|
+
exportSingleVideo(phAsset, galleryVC: galleryVC, tempDir: tempDir, resolve: resolve, reject: reject)
|
|
1814
|
+
return
|
|
1815
|
+
}
|
|
1463
1816
|
let maxWidth = targetMaxWidth ?? galleryVC?.maxWidth ?? 1024
|
|
1464
1817
|
let maxHeight = targetMaxHeight ?? galleryVC?.maxHeight ?? 1024
|
|
1465
1818
|
let format = galleryVC?.outputFormat ?? "webp"
|
|
@@ -1906,6 +2259,7 @@ fileprivate extension RNAttachmentKit {
|
|
|
1906
2259
|
|
|
1907
2260
|
editorVC.onEditDeleted = { [weak currentGallery] asset in
|
|
1908
2261
|
currentGallery?.editedImages.removeValue(forKey: asset.localIdentifier)
|
|
2262
|
+
currentGallery?.videoEditStates.removeValue(forKey: asset.localIdentifier) // 동영상 편집도 기본으로 돌아간 것
|
|
1909
2263
|
}
|
|
1910
2264
|
|
|
1911
2265
|
editorVC.modalPresentationStyle = .overFullScreen
|
|
@@ -1921,7 +2275,22 @@ fileprivate extension RNAttachmentKit {
|
|
|
1921
2275
|
}
|
|
1922
2276
|
}
|
|
1923
2277
|
editorVC.croppedImages = initialCroppedImages
|
|
2278
|
+
// 동영상 페이지: 품질 단계·용량 상한 + 갤러리가 들고 있는 상태. 바뀌면 바로 갤러리에 반영 (전송 때 VideoExporter 가 읽음)
|
|
2279
|
+
editorVC.videoOptions = VideoExportOptions(dictionary: options["video"] as? [String: Any])
|
|
2280
|
+
var initialVideoStates: [Int: VideoEditState] = [:]
|
|
2281
|
+
for (i, a) in editorAssets.enumerated() {
|
|
2282
|
+
if let st = currentGallery.videoEditStates[a.localIdentifier] { initialVideoStates[i] = st }
|
|
2283
|
+
}
|
|
2284
|
+
editorVC.videoEditStates = initialVideoStates
|
|
2285
|
+
editorVC.onVideoStateChanged = { [weak currentGallery] asset, state in
|
|
2286
|
+
currentGallery?.videoEditStates[asset.localIdentifier] = state
|
|
2287
|
+
}
|
|
1924
2288
|
|
|
2289
|
+
if sourceFrame == .zero {
|
|
2290
|
+
// 기준 칸이 없을 때(칸이 화면 밖 등): 기본 전환(아래로 미끄러짐)은 끌어내려 닫을 때 위로 치워 둔 상단 바가
|
|
2291
|
+
// 화면과 함께 내려오며 보이므로 페이드로
|
|
2292
|
+
editorVC.modalTransitionStyle = .crossDissolve
|
|
2293
|
+
}
|
|
1925
2294
|
if sourceFrame != .zero {
|
|
1926
2295
|
self.editorTransitionDelegate = ImageEditorTransitionDelegate()
|
|
1927
2296
|
self.editorTransitionDelegate?.sourceFrame = sourceFrame
|
|
@@ -1961,7 +2330,10 @@ fileprivate extension RNAttachmentKit {
|
|
|
1961
2330
|
// (호출 중인 객체이므로 다음 런루프에서)
|
|
1962
2331
|
DispatchQueue.main.async { self?.editorTransitionDelegate = nil }
|
|
1963
2332
|
}
|
|
1964
|
-
|
|
2333
|
+
// 칸은 열기 애니메이션의 첫 프레임에 비움 (누르자마자 비우면 편집 화면이 준비되는 동안 빈 칸이 번쩍임)
|
|
2334
|
+
self.editorTransitionDelegate?.onPresentationStarted = { [weak currentGallery] in
|
|
2335
|
+
currentGallery?.setHiddenAsset(asset)
|
|
2336
|
+
}
|
|
1965
2337
|
|
|
1966
2338
|
editorVC.transitioningDelegate = self.editorTransitionDelegate
|
|
1967
2339
|
}
|
|
@@ -2085,3 +2457,69 @@ extension RNAttachmentKit: UIViewControllerTransitioningDelegate {
|
|
|
2085
2457
|
return zoomAnimator
|
|
2086
2458
|
}
|
|
2087
2459
|
}
|
|
2460
|
+
|
|
2461
|
+
// MARK: - 전송 처리 상태 (앱에서 동영상 취소)
|
|
2462
|
+
|
|
2463
|
+
/// openGallery 결과 처리 한 번. cancelVideoProcessing(index) → 그 동영상만(또는 전부) 취소
|
|
2464
|
+
private final class MediaProcessingJob {
|
|
2465
|
+
private let lock = NSLock()
|
|
2466
|
+
private var cancelAll = false
|
|
2467
|
+
private var cancelled = Set<Int>()
|
|
2468
|
+
private var currentIndex: Int?
|
|
2469
|
+
private var currentExporter: VideoExporter?
|
|
2470
|
+
|
|
2471
|
+
func cancel(index: Int?) {
|
|
2472
|
+
lock.lock(); defer { lock.unlock() }
|
|
2473
|
+
if let i = index {
|
|
2474
|
+
cancelled.insert(i)
|
|
2475
|
+
if currentIndex == i { currentExporter?.cancel() }
|
|
2476
|
+
} else {
|
|
2477
|
+
cancelAll = true
|
|
2478
|
+
currentExporter?.cancel()
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
func isCancelled(_ i: Int) -> Bool {
|
|
2483
|
+
lock.lock(); defer { lock.unlock() }
|
|
2484
|
+
return cancelAll || cancelled.contains(i)
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
func begin(index: Int, exporter: VideoExporter) {
|
|
2488
|
+
lock.lock(); defer { lock.unlock() }
|
|
2489
|
+
currentIndex = index
|
|
2490
|
+
currentExporter = exporter
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
func end() {
|
|
2494
|
+
lock.lock(); defer { lock.unlock() }
|
|
2495
|
+
currentIndex = nil
|
|
2496
|
+
currentExporter = nil
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
/// onVideoProgress 를 1% 이상 바뀌고 0.1초 지났을 때만 (브릿지가 넘치지 않게). 0 과 1 은 항상
|
|
2501
|
+
private final class ProgressThrottle {
|
|
2502
|
+
private let lock = NSLock()
|
|
2503
|
+
private var last: Double = -1
|
|
2504
|
+
private var lastTime: CFTimeInterval = 0
|
|
2505
|
+
|
|
2506
|
+
func shouldSend(_ p: Double) -> Bool {
|
|
2507
|
+
lock.lock(); defer { lock.unlock() }
|
|
2508
|
+
let now = CACurrentMediaTime()
|
|
2509
|
+
guard p >= 1 || last < 0 || (p - last >= 0.01 && now - lastTime >= 0.1) else { return false }
|
|
2510
|
+
if p >= 1 && last >= 1 { return false }
|
|
2511
|
+
last = p
|
|
2512
|
+
lastTime = now
|
|
2513
|
+
return true
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
/// 스레드 사이에서 읽고 쓰는 Bool
|
|
2518
|
+
private final class AtomicFlag {
|
|
2519
|
+
private let lock = NSLock()
|
|
2520
|
+
private var v = false
|
|
2521
|
+
var value: Bool {
|
|
2522
|
+
get { lock.lock(); defer { lock.unlock() }; return v }
|
|
2523
|
+
set { lock.lock(); v = newValue; lock.unlock() }
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
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>
|