react-native-attachment-kit 1.0.2 → 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 +121 -0
- package/README.md +446 -206
- package/android/build.gradle +17 -0
- package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
- package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +587 -22
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +592 -21
- package/ios/RNAttachmentKit.m +62 -0
- package/ios/RNAttachmentKit.swift +725 -29
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Headers/RNAttachmentKit-Swift.h +51 -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 +51876 -18835
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +310 -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 +310 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_reflect~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_reflect~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_back~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_back~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_blur~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_blur~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check_small~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check_small~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_close~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_close~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_text_add~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_text_add~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +195 -63
- 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 +102 -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 +51876 -18835
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +310 -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 +310 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +51876 -18835
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +310 -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 +310 -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/ToolbarIcons/ic_crop_reflect~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_crop_reflect~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_back~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_back~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_blur~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_blur~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check_small~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check_small~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_close~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_close~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_text_add~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_text_add~light.webp +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 +205 -73
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_reflect~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_reflect~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_rotate~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_rotate~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_delete~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_delete~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_forward~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_forward~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_reward~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_reward~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_back~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_back~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_blur~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_blur~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_check_small~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_check_small~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_check~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_check~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_close~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_close~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_crop~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_crop~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_drawing~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_drawing~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_effect~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_effect~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_emoji~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_emoji~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_eraser~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_eraser~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_mosaic~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_mosaic~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_text~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_text~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_text_add~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_text_add~light.webp +0 -0
- package/package.json +1 -1
- package/react-native-attachment-kit.podspec +1 -1
- package/src/NativeAttachmentKit.ts +15 -0
- package/src/index.d.ts +5 -56
- package/src/index.ts +301 -37
- package/src/types.ts +380 -11
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import UIKit
|
|
9
9
|
import Photos
|
|
10
10
|
import ImageIO
|
|
11
|
+
import CoreLocation
|
|
11
12
|
import RNAttachmentKit
|
|
12
13
|
|
|
13
14
|
#if RCT_NEW_ARCH_ENABLED
|
|
@@ -48,6 +49,13 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
48
49
|
private var editorTransitionDelegate: ImageEditorTransitionDelegate?
|
|
49
50
|
/// 열려 있는 미디어 갤러리 (galleryId → 화면)
|
|
50
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
|
|
51
59
|
|
|
52
60
|
// MARK: - Module Setup
|
|
53
61
|
|
|
@@ -64,7 +72,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
64
72
|
@objc
|
|
65
73
|
override func supportedEvents() -> [String]! {
|
|
66
74
|
return ["onSelectionChange", "onImageProcessed", "onPageSelected", "onViewerWillClose", "onImageDeleted",
|
|
67
|
-
"onGalleryItemDeleted", "onGalleryNeedPage", "onGalleryClosed", "onGalleryGridSizeChanged"
|
|
75
|
+
"onGalleryItemDeleted", "onGalleryNeedPage", "onGalleryClosed", "onGalleryGridSizeChanged",
|
|
76
|
+
"onLocationSelected", "onFilesSelected", "onAIUsage", "onAIChargeRequired", "onVideoProgress",
|
|
77
|
+
"onMapViewerWillClose", "onProcessingStart"]
|
|
68
78
|
}
|
|
69
79
|
|
|
70
80
|
// MARK: - Public Methods
|
|
@@ -171,6 +181,230 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
171
181
|
}
|
|
172
182
|
}
|
|
173
183
|
|
|
184
|
+
// MARK: - Map Viewer (위치 메시지 → 지도)
|
|
185
|
+
|
|
186
|
+
private weak var activeMapViewerVC: MapViewerViewController?
|
|
187
|
+
/// updateMapViewerSourceRect 로 갱신되는 닫힘 위치 (window points)
|
|
188
|
+
private var mapViewerSourceRect: CGRect?
|
|
189
|
+
|
|
190
|
+
private static func cgFloat(_ v: Any?) -> CGFloat? {
|
|
191
|
+
if let n = v as? NSNumber { return CGFloat(truncating: n) }
|
|
192
|
+
if let d = v as? Double { return CGFloat(d) }
|
|
193
|
+
return nil
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private static func parseRect(_ v: Any?) -> CGRect? {
|
|
197
|
+
guard let d = v as? NSDictionary,
|
|
198
|
+
let x = cgFloat(d["x"]), let y = cgFloat(d["y"]),
|
|
199
|
+
let w = cgFloat(d["width"]), let h = cgFloat(d["height"]), w > 0, h > 0 else { return nil }
|
|
200
|
+
return CGRect(x: x, y: y, width: w, height: h)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// 말풍선 이미지(정적 지도 스냅샷) 로드. 메모리/디스크 캐시 → file:// → 다운로드 순.
|
|
204
|
+
/// 실패하거나 너무 오래 걸리면 nil (이미지 없이 지도만 엶)
|
|
205
|
+
/// completion(이미지, 마커 뺀 이미지인지). renderLocationThumbnail 결과면 마커 뺀 짝을 씀 → 뷰어가 마커를 따로 그려
|
|
206
|
+
/// 말풍선 속 크기 ↔ 지도 마커 크기로 자연스럽게 바꿈
|
|
207
|
+
private func loadMapSourceImage(_ uri: String?, completion: @escaping (UIImage?, Bool) -> Void) {
|
|
208
|
+
guard let uri = uri, !uri.isEmpty else { completion(nil, false); return }
|
|
209
|
+
if uri.hasPrefix("file://") || uri.hasPrefix("/") {
|
|
210
|
+
let path = uri.hasPrefix("file://") ? (URL(string: uri)?.path ?? String(uri.dropFirst(7))) : uri
|
|
211
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
212
|
+
if let base = LocationThumbnailRenderer.markerlessFile(for: path), let img = UIImage(contentsOfFile: base) {
|
|
213
|
+
DispatchQueue.main.async { completion(img, true) }
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
let image = UIImage(contentsOfFile: path)
|
|
217
|
+
DispatchQueue.main.async { completion(image, false) }
|
|
218
|
+
}
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
if let cached = ViewerImageCache.shared.getMemoryImage(for: uri) { completion(cached, false); return }
|
|
222
|
+
var done = false
|
|
223
|
+
let finish: (UIImage?) -> Void = { image in
|
|
224
|
+
DispatchQueue.main.async {
|
|
225
|
+
guard !done else { return }
|
|
226
|
+
done = true
|
|
227
|
+
completion(image, false)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// 다운로드가 늦으면 이미지 없이 바로 엶 (탭 반응이 늦어지지 않게)
|
|
231
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { finish(nil) }
|
|
232
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
233
|
+
if let disk = ViewerImageCache.shared.getDiskImageSynchronously(for: uri) { finish(disk); return }
|
|
234
|
+
RemoteImageViewerViewController.prefetchImage(from: uri) { finish($0) }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@objc
|
|
239
|
+
func openMapViewer(_ options: NSDictionary,
|
|
240
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
241
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
242
|
+
guard let lat = RNAttachmentKit.cgFloat(options["latitude"]),
|
|
243
|
+
let lng = RNAttachmentKit.cgFloat(options["longitude"]) else {
|
|
244
|
+
reject("ERROR", "latitude and longitude are required", nil)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
let sourceRect = RNAttachmentKit.parseRect(options["sourceRect"])
|
|
248
|
+
let imageUri = options["sourceImageUri"] as? String
|
|
249
|
+
DispatchQueue.main.async {
|
|
250
|
+
// 위치 정보만으로 여는 경우엔 이미지 로드를 기다리지 않음
|
|
251
|
+
let load: (@escaping (UIImage?, Bool) -> Void) -> Void = { done in
|
|
252
|
+
if sourceRect == nil { done(nil, false) } else { self.loadMapSourceImage(imageUri, completion: done) }
|
|
253
|
+
}
|
|
254
|
+
load { image, markerless in
|
|
255
|
+
guard let root = self.getRootViewController() else {
|
|
256
|
+
reject("ERROR", "Unable to get root view controller", nil)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
var accent = UIColor(red: 1, green: 0.42, blue: 0.21, alpha: 1)
|
|
260
|
+
if let hex = options["themeColor"] as? String, let c = UIColor(hexString: hex) { accent = c }
|
|
261
|
+
let vc = MapViewerViewController(
|
|
262
|
+
latitude: Double(lat), longitude: Double(lng),
|
|
263
|
+
title: options["title"] as? String,
|
|
264
|
+
address: options["address"] as? String,
|
|
265
|
+
markerType: options["markerType"] as? String,
|
|
266
|
+
placeCategory: options["placeCategory"] as? String,
|
|
267
|
+
accentColor: accent,
|
|
268
|
+
languageCode: options["languageCode"] as? String ?? "en")
|
|
269
|
+
vc.overrideUserInterfaceStyle = RNAttachmentKit.interfaceStyle(options)
|
|
270
|
+
vc.showsDirections = (options["showDirections"] as? Bool) ?? true
|
|
271
|
+
vc.addressLanguageCode = (options["addressLanguageCode"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
272
|
+
if let span = RNAttachmentKit.cgFloat(options["spanMeters"]), span > 0 { vc.spanMeters = Double(span) }
|
|
273
|
+
if let r = RNAttachmentKit.cgFloat(options["sourceCornerRadius"]) { vc.sourceCornerRadius = r }
|
|
274
|
+
self.mapViewerSourceRect = sourceRect
|
|
275
|
+
if let rect = sourceRect {
|
|
276
|
+
vc.sourceRect = rect
|
|
277
|
+
vc.sourceImage = image
|
|
278
|
+
vc.sourceImageIsMarkerless = markerless && image != nil
|
|
279
|
+
vc.sourceRectProvider = { [weak self] in self?.mapViewerSourceRect }
|
|
280
|
+
}
|
|
281
|
+
vc.onWillClose = { [weak self] dur in
|
|
282
|
+
self?.sendEvent(withName: "onMapViewerWillClose", body: ["durationMs": Int(dur * 1000)])
|
|
283
|
+
}
|
|
284
|
+
vc.onClosed = { [weak self] in
|
|
285
|
+
self?.mapViewerSourceRect = nil
|
|
286
|
+
resolve(nil)
|
|
287
|
+
}
|
|
288
|
+
self.activeMapViewerVC = vc
|
|
289
|
+
root.present(vc, animated: sourceRect == nil)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@objc
|
|
295
|
+
func updateMapViewerSourceRect(_ rect: NSDictionary,
|
|
296
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
297
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
298
|
+
let parsed = RNAttachmentKit.parseRect(rect)
|
|
299
|
+
DispatchQueue.main.async {
|
|
300
|
+
if self.activeMapViewerVC != nil { self.mapViewerSourceRect = parsed }
|
|
301
|
+
resolve(nil)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// 위치 메시지 말풍선 지도 썸네일을 이 기기에서 그려 캐시 (보낸 이미지 없이 위치 정보만으로)
|
|
306
|
+
@objc
|
|
307
|
+
func renderLocationThumbnail(_ options: NSDictionary,
|
|
308
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
309
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
310
|
+
guard let lat = RNAttachmentKit.cgFloat(options["latitude"]),
|
|
311
|
+
let lng = RNAttachmentKit.cgFloat(options["longitude"]),
|
|
312
|
+
let w = RNAttachmentKit.cgFloat(options["width"]), w > 0 else {
|
|
313
|
+
reject("E_INVALID_ARGS", "latitude, longitude and width are required", nil)
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
DispatchQueue.main.async {
|
|
317
|
+
// 비율(aspect)이 있으면 세로는 가로에서 계산, 없으면 height 그대로
|
|
318
|
+
guard let h = LocationThumbnailRenderer.height(forWidth: w, aspect: options["aspect"] as? String)
|
|
319
|
+
?? RNAttachmentKit.cgFloat(options["height"]), h > 0 else {
|
|
320
|
+
reject("E_INVALID_ARGS", "height (or aspect) is required", nil)
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
var accent = UIColor(red: 1, green: 0.42, blue: 0.21, alpha: 1)
|
|
324
|
+
if let hex = options["themeColor"] as? String, let c = UIColor(hexString: hex) { accent = c }
|
|
325
|
+
var style = RNAttachmentKit.interfaceStyle(options)
|
|
326
|
+
if style == .unspecified {
|
|
327
|
+
// "system": 지금 앱 화면 모드
|
|
328
|
+
style = UIApplication.shared.connectedScenes.compactMap { ($0 as? UIWindowScene)?.windows.first(where: \.isKeyWindow) }
|
|
329
|
+
.first?.traitCollection.userInterfaceStyle ?? UITraitCollection.current.userInterfaceStyle
|
|
330
|
+
}
|
|
331
|
+
var me: CLLocationCoordinate2D?
|
|
332
|
+
if let mlat = RNAttachmentKit.cgFloat(options["myLatitude"]), let mlng = RNAttachmentKit.cgFloat(options["myLongitude"]) {
|
|
333
|
+
me = CLLocationCoordinate2D(latitude: Double(mlat), longitude: Double(mlng))
|
|
334
|
+
}
|
|
335
|
+
let span = RNAttachmentKit.cgFloat(options["spanMeters"]).map { Double($0) }
|
|
336
|
+
var req = LocationThumbnailRequest(
|
|
337
|
+
latitude: Double(lat), longitude: Double(lng),
|
|
338
|
+
size: CGSize(width: w, height: h), scale: min(UIScreen.main.scale, 3),
|
|
339
|
+
isDark: style == .dark,
|
|
340
|
+
markerType: options["markerType"] as? String,
|
|
341
|
+
placeCategory: options["placeCategory"] as? String,
|
|
342
|
+
spanMeters: (span ?? 0) > 0 ? span : nil,
|
|
343
|
+
accentColor: accent,
|
|
344
|
+
myLocation: me)
|
|
345
|
+
// 결과 이미지 최대 크기 (px). 둘 다 없으면 512 × 512, 하나만 주면 다른 쪽은 제한 없음
|
|
346
|
+
// (예: maxWidth 512 만 주고 세로가 기본 512 에 잘려 작아지는 일이 없게)
|
|
347
|
+
let maxW = RNAttachmentKit.cgFloat(options["maxWidth"]).flatMap { $0 > 0 ? $0 : nil }
|
|
348
|
+
let maxH = RNAttachmentKit.cgFloat(options["maxHeight"]).flatMap { $0 > 0 ? $0 : nil }
|
|
349
|
+
if maxW != nil || maxH != nil {
|
|
350
|
+
req.maxPixelWidth = maxW ?? .greatestFiniteMagnitude
|
|
351
|
+
req.maxPixelHeight = maxH ?? .greatestFiniteMagnitude
|
|
352
|
+
}
|
|
353
|
+
LocationThumbnailRenderer.render(req) { result in
|
|
354
|
+
switch result {
|
|
355
|
+
case .success(let dict): resolve(dict)
|
|
356
|
+
case .failure(let error): reject("E_THUMBNAIL_FAILED", error.localizedDescription, error)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// MARK: - Reverse geocode (기기 지오코더, 디스크 캐시)
|
|
363
|
+
|
|
364
|
+
@objc
|
|
365
|
+
func reverseGeocode(_ options: NSDictionary,
|
|
366
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
367
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
368
|
+
let lat = (options["latitude"] as? NSNumber)?.doubleValue ?? .nan
|
|
369
|
+
let lng = (options["longitude"] as? NSNumber)?.doubleValue ?? .nan
|
|
370
|
+
let lang = (options["languageCode"] as? String).flatMap { $0.isEmpty ? nil : $0 } ?? "en"
|
|
371
|
+
ReverseGeocoder.shared.reverseGeocode(latitude: lat, longitude: lng, languageCode: lang) { result in
|
|
372
|
+
switch result {
|
|
373
|
+
case .success(let r): resolve(r.dictionary)
|
|
374
|
+
case .failure(let e): reject(e.code, e.message, nil)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
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
|
+
|
|
393
|
+
@objc
|
|
394
|
+
func clearReverseGeocodeCache(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
395
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
396
|
+
ReverseGeocoder.shared.clearCache { resolve(nil) }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
@objc
|
|
400
|
+
func clearLocationThumbnailCache(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
401
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
402
|
+
DispatchQueue.global(qos: .utility).async {
|
|
403
|
+
LocationThumbnailRenderer.clearCache()
|
|
404
|
+
resolve(nil)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
174
408
|
@objc
|
|
175
409
|
func openViewer(_ options: NSDictionary,
|
|
176
410
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
@@ -184,6 +418,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
184
418
|
let placeholderImages = options["placeholderImages"] as? [String]
|
|
185
419
|
// cacheFiles: images 와 같은 순서의 로컬 캐시 파일 경로 (null 항목은 NSNull 로 옴 → 빈 문자열)
|
|
186
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) } ?? []
|
|
187
423
|
let themeColorHex = options["themeColor"] as? String
|
|
188
424
|
let languageCode = options["languageCode"] as? String ?? "en"
|
|
189
425
|
let viewerTitle = options["title"] as? String
|
|
@@ -267,6 +503,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
267
503
|
viewerVC.viewerTitle = viewerTitle
|
|
268
504
|
viewerVC.placeholderImages = placeholderImages
|
|
269
505
|
viewerVC.cacheFiles = cacheFiles
|
|
506
|
+
viewerVC.videos = viewerVideos
|
|
270
507
|
if let hex = themeColorHex, let color = UIColor(hexString: hex) {
|
|
271
508
|
viewerVC.themeColor = color
|
|
272
509
|
}
|
|
@@ -560,30 +797,42 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
560
797
|
resolve: @escaping RCTPromiseResolveBlock,
|
|
561
798
|
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
562
799
|
let startTime = CFAbsoluteTimeGetCurrent()
|
|
800
|
+
#if DEBUG
|
|
563
801
|
print("✅ [DEBUG] received options: \(options)")
|
|
564
802
|
print("🔵 [DEBUG] openGallery 시작 - 시간: \(startTime)")
|
|
565
|
-
|
|
803
|
+
#endif
|
|
804
|
+
|
|
805
|
+
// 예전에 파일/위치 탭에서 임시 폴더에 복사해 두고 24시간이 지난 것 정리 (백그라운드, UI 와 무관)
|
|
806
|
+
DispatchQueue.global(qos: .utility).async {
|
|
807
|
+
TempFileCleaner.cleanupStaleTempFiles()
|
|
808
|
+
}
|
|
809
|
+
|
|
566
810
|
// Main thread에서 실행
|
|
567
811
|
DispatchQueue.main.async { [weak self] () -> Void in
|
|
568
812
|
guard let self = self else {
|
|
569
813
|
reject("ERROR", "Module deallocated", nil)
|
|
570
814
|
return
|
|
571
815
|
}
|
|
572
|
-
|
|
816
|
+
|
|
817
|
+
#if DEBUG
|
|
573
818
|
print("🔵 [DEBUG] workItem 실행 시작 - 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - startTime))초")
|
|
574
|
-
|
|
819
|
+
#endif
|
|
820
|
+
|
|
575
821
|
let step1 = CFAbsoluteTimeGetCurrent()
|
|
576
822
|
guard let rootViewController = self.getRootViewController() else {
|
|
577
823
|
print("❌ [DEBUG] getRootViewController 실패")
|
|
578
824
|
reject("ERROR", "Unable to get root view controller", nil)
|
|
579
825
|
return
|
|
580
826
|
}
|
|
827
|
+
#if DEBUG
|
|
581
828
|
print("🔵 [DEBUG] getRootViewController 완료 - 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - step1))초")
|
|
582
|
-
|
|
829
|
+
#endif
|
|
830
|
+
|
|
583
831
|
let step2 = CFAbsoluteTimeGetCurrent()
|
|
584
|
-
print("🔵 [DEBUG] GalleryViewController 생성 시작...")
|
|
585
832
|
let galleryVC = GalleryViewController()
|
|
833
|
+
#if DEBUG
|
|
586
834
|
print("🔵 [DEBUG] GalleryViewController 생성 완료 - 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - step2))초")
|
|
835
|
+
#endif
|
|
587
836
|
|
|
588
837
|
// 현재 갤러리 참조 저장
|
|
589
838
|
self.currentGalleryVC = galleryVC
|
|
@@ -679,6 +928,8 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
679
928
|
editorVC.onCancel = { [weak currentGallery] in
|
|
680
929
|
currentGallery?.selectedAssets.removeAll()
|
|
681
930
|
currentGallery?.refreshSelectionAfterEdit()
|
|
931
|
+
// 결과 전달 책임이 갤러리로 돌아옴 → 닫기가 다시 취소(reject)를 보낼 수 있게
|
|
932
|
+
currentGallery?.hasReturnedImages = false
|
|
682
933
|
}
|
|
683
934
|
|
|
684
935
|
var savedDetent: UISheetPresentationController.Detent.Identifier? = nil
|
|
@@ -714,7 +965,45 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
714
965
|
}
|
|
715
966
|
}
|
|
716
967
|
|
|
717
|
-
|
|
968
|
+
// 첨부 메뉴(갤러리 / 위치 / 파일). 기본 false — 켜지 않으면 기존과 같음
|
|
969
|
+
galleryVC.showsAttachMenu = options["showAttachMenu"] as? Bool ?? false
|
|
970
|
+
// 위치 보내기 때 지도 캡처 이미지 (기본 true). false 면 위치 정보만 — 받는 쪽이 renderLocationThumbnail 로 그림
|
|
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
|
|
980
|
+
// 위치 탭에서 보내기 → onLocationSelected 이벤트로 결과를 넘기고, openGallery 는 빈 배열로 끝냄
|
|
981
|
+
galleryVC.onLocationSelected = { [weak self, weak galleryVC] location in
|
|
982
|
+
self?.currentGalleryVC = nil
|
|
983
|
+
self?.sendEvent(withName: "onLocationSelected", body: location.dictionary)
|
|
984
|
+
resolve([])
|
|
985
|
+
galleryVC?.releaseAfterReturn()
|
|
986
|
+
}
|
|
987
|
+
// 파일 탭에서 보내기 → onFilesSelected 이벤트 { files: [{uri, path, name, size, mimeType}] }, openGallery 는 빈 배열
|
|
988
|
+
// 일부 파일만 복사에 실패하면 (추가 필드) failed: [{name, error}] 를 함께 보냄 (files 는 그대로)
|
|
989
|
+
galleryVC.onFilesSelectedWithFailures = { [weak self, weak galleryVC] files, failures in
|
|
990
|
+
self?.currentGalleryVC = nil
|
|
991
|
+
var body: [String: Any] = ["files": files.map { $0.dictionary }]
|
|
992
|
+
if !failures.isEmpty { body["failed"] = failures.map { $0.dictionary } }
|
|
993
|
+
self?.sendEvent(withName: "onFilesSelected", body: body)
|
|
994
|
+
resolve([])
|
|
995
|
+
galleryVC?.releaseAfterReturn()
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ⚠️ 클로저 안에서 galleryVC 를 강하게 잡으면 galleryVC → onImagesSelected → galleryVC 순환 참조가 되어
|
|
999
|
+
// (성공 경로는 viewDidDisappear 가 정리를 deinit 으로 미루므로) 갤러리가 영영 해제되지 않았습니다.
|
|
1000
|
+
// 필요한 값은 미리 상수로 꺼내고, galleryVC 는 weak 로만 씁니다.
|
|
1001
|
+
let galleryMaxWidth = galleryVC.maxWidth
|
|
1002
|
+
let galleryMaxHeight = galleryVC.maxHeight
|
|
1003
|
+
let galleryFormat = galleryVC.outputFormat // "webp" | "jpg" | "png"
|
|
1004
|
+
let galleryAccent = galleryVC.parsedThemeColor
|
|
1005
|
+
let galleryLanguage = galleryVC.languageCode
|
|
1006
|
+
galleryVC.onImagesSelected = { [weak self, weak galleryVC] selectedImages in
|
|
718
1007
|
guard let self = self else { resolve([]); return }
|
|
719
1008
|
|
|
720
1009
|
guard !selectedImages.isEmpty else {
|
|
@@ -725,21 +1014,137 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
725
1014
|
|
|
726
1015
|
var results: [[String: Any]] = []
|
|
727
1016
|
let tempDir = self.prepareCacheDirectory()
|
|
728
|
-
let maxWidth =
|
|
729
|
-
let maxHeight =
|
|
730
|
-
let format =
|
|
1017
|
+
let maxWidth = galleryMaxWidth
|
|
1018
|
+
let maxHeight = galleryMaxHeight
|
|
1019
|
+
let format = galleryFormat
|
|
731
1020
|
let mimeType = self.mimeType(for: format)
|
|
732
1021
|
let needResize = (maxWidth > 0 || maxHeight > 0)
|
|
1022
|
+
let total = selectedImages.count
|
|
733
1023
|
|
|
734
1024
|
let resultsLock = NSLock()
|
|
735
|
-
|
|
1025
|
+
// 처리한 이미지는 바로 놓아 주도록 (전체 UIImage 배열을 끝까지 들고 있지 않게) 옵셔널 배열로 옮김
|
|
1026
|
+
var pending: [(PHAsset?, UIImage)?] = selectedImages.map { $0 }
|
|
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
|
+
|
|
736
1050
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
737
|
-
|
|
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
|
+
])
|
|
1061
|
+
for index in 0..<total {
|
|
1062
|
+
guard let pair = pending[index] else { continue }
|
|
1063
|
+
pending[index] = nil
|
|
1064
|
+
exporterLock.lock(); let stop = cancelledAll; exporterLock.unlock()
|
|
1065
|
+
if stop { break }
|
|
1066
|
+
autoreleasepool {
|
|
738
1067
|
let (asset, image) = pair
|
|
739
1068
|
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
740
1069
|
let originalSize = image.size
|
|
741
1070
|
|
|
742
|
-
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 {
|
|
743
1148
|
// ✅ 1순위: PHAsset
|
|
744
1149
|
let originalUri = "ph://\(phAsset.localIdentifier)"
|
|
745
1150
|
|
|
@@ -778,8 +1183,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
778
1183
|
|
|
779
1184
|
self.sendEvent(withName: "onImageProcessed", body: [
|
|
780
1185
|
"index": index,
|
|
781
|
-
"total":
|
|
782
|
-
"image": currentResult
|
|
1186
|
+
"total": total,
|
|
1187
|
+
"image": currentResult,
|
|
1188
|
+
"processingId": processingId
|
|
783
1189
|
])
|
|
784
1190
|
} else {
|
|
785
1191
|
// ✅ 2순위: 카메라 촬영본
|
|
@@ -828,15 +1234,31 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
828
1234
|
|
|
829
1235
|
self.sendEvent(withName: "onImageProcessed", body: [
|
|
830
1236
|
"index": index,
|
|
831
|
-
"total":
|
|
832
|
-
"image": currentResult
|
|
1237
|
+
"total": total,
|
|
1238
|
+
"image": currentResult,
|
|
1239
|
+
"processingId": processingId
|
|
833
1240
|
])
|
|
834
1241
|
}
|
|
1242
|
+
} // end autoreleasepool
|
|
835
1243
|
} // end for
|
|
836
1244
|
|
|
837
1245
|
DispatchQueue.main.async {
|
|
838
|
-
|
|
839
|
-
|
|
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)
|
|
840
1262
|
}
|
|
841
1263
|
}
|
|
842
1264
|
}
|
|
@@ -877,16 +1299,88 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
877
1299
|
}
|
|
878
1300
|
}
|
|
879
1301
|
}
|
|
1302
|
+
#if DEBUG
|
|
880
1303
|
print("🔵 [DEBUG] Sheet 설정 완료 - 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - step3))초")
|
|
1304
|
+
#endif
|
|
881
1305
|
|
|
882
1306
|
let step4 = CFAbsoluteTimeGetCurrent()
|
|
883
|
-
print("🔵 [DEBUG] present 시작...")
|
|
884
1307
|
rootViewController.present(navigationController, animated: true)
|
|
1308
|
+
#if DEBUG
|
|
885
1309
|
print("🔵 [DEBUG] present 완료 - 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - step4))초")
|
|
886
1310
|
print("✅ [DEBUG] openGallery 전체 완료 - 총 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - startTime))초")
|
|
1311
|
+
#endif
|
|
887
1312
|
}
|
|
888
1313
|
}
|
|
889
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
|
+
|
|
890
1384
|
@objc
|
|
891
1385
|
func closeGallery(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
892
1386
|
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
@@ -902,6 +1396,13 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
902
1396
|
return
|
|
903
1397
|
}
|
|
904
1398
|
|
|
1399
|
+
// openGallery 약속이 아직 안 풀렸으면 (닫기 버튼과 같은 경로로) 취소를 먼저 전달 → reject("CANCELLED")
|
|
1400
|
+
// 안 그러면 아래 cleanupMemory 가 콜백을 지워 버려 JS 약속이 영영 안 풀렸습니다.
|
|
1401
|
+
if !galleryVC.hasReturnedImages {
|
|
1402
|
+
galleryVC.hasReturnedImages = true
|
|
1403
|
+
galleryVC.onImagesSelected?([])
|
|
1404
|
+
}
|
|
1405
|
+
|
|
905
1406
|
// 갤러리 닫기 전 강제 정리
|
|
906
1407
|
galleryVC.forceCleanupBeforeDismiss()
|
|
907
1408
|
|
|
@@ -1032,14 +1533,18 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1032
1533
|
let options: [CFString: Any] = [kCGImageDestinationLossyCompressionQuality: quality]
|
|
1033
1534
|
CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
|
|
1034
1535
|
if CGImageDestinationFinalize(destination) {
|
|
1536
|
+
#if DEBUG
|
|
1035
1537
|
NSLog("✅ [encodeWebP] iOS 16 네이티브 WebP 인코딩 성공 - %d bytes", data.length)
|
|
1538
|
+
#endif
|
|
1036
1539
|
return data as Data
|
|
1037
1540
|
}
|
|
1038
1541
|
}
|
|
1039
1542
|
}
|
|
1040
1543
|
}
|
|
1041
1544
|
|
|
1545
|
+
#if DEBUG
|
|
1042
1546
|
NSLog("🟣 [encodeWebP] SDImageWebPCoder 런타임 폴백 시도")
|
|
1547
|
+
#endif
|
|
1043
1548
|
// iOS 14, 15이거나 네이티브 실패 시 SDImageWebPCoder 런타임 호출
|
|
1044
1549
|
if let coderClass = NSClassFromString("SDImageWebPCoder") as? NSObject.Type {
|
|
1045
1550
|
if coderClass.responds(to: NSSelectorFromString("sharedCoder")) {
|
|
@@ -1057,7 +1562,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1057
1562
|
// format: 3 (SDImageFormatWebP)
|
|
1058
1563
|
let options: [String: Any] = ["encodeCompressionQuality": quality]
|
|
1059
1564
|
if let data = encode(sharedCoder, encodeSel, image, 3, options) {
|
|
1565
|
+
#if DEBUG
|
|
1060
1566
|
NSLog("✅ [encodeWebP] SDImageWebPCoder 런타임 인코딩 성공 - %d bytes", data.count)
|
|
1567
|
+
#endif
|
|
1061
1568
|
return data
|
|
1062
1569
|
} else {
|
|
1063
1570
|
NSLog("❌ [encodeWebP] SDImageWebPCoder 런타임 인코딩 실패 (nil 반환)")
|
|
@@ -1181,9 +1688,105 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1181
1688
|
}
|
|
1182
1689
|
|
|
1183
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
|
+
|
|
1184
1782
|
|
|
1185
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) {
|
|
1186
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
|
+
}
|
|
1187
1790
|
let maxWidth = targetMaxWidth ?? galleryVC?.maxWidth ?? 1024
|
|
1188
1791
|
let maxHeight = targetMaxHeight ?? galleryVC?.maxHeight ?? 1024
|
|
1189
1792
|
let format = galleryVC?.outputFormat ?? "webp"
|
|
@@ -1237,6 +1840,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1237
1840
|
DispatchQueue.main.async {
|
|
1238
1841
|
self.currentGalleryVC = nil
|
|
1239
1842
|
reject("ERROR", "Failed to load image from editor", nil)
|
|
1843
|
+
galleryVC?.releaseAfterReturn()
|
|
1240
1844
|
}
|
|
1241
1845
|
return
|
|
1242
1846
|
}
|
|
@@ -1275,6 +1879,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1275
1879
|
DispatchQueue.main.async {
|
|
1276
1880
|
self.currentGalleryVC = nil
|
|
1277
1881
|
resolve([result])
|
|
1882
|
+
galleryVC?.releaseAfterReturn()
|
|
1278
1883
|
}
|
|
1279
1884
|
}
|
|
1280
1885
|
}
|
|
@@ -1289,10 +1894,12 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1289
1894
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
1290
1895
|
let photoManager = PhotoManager.shared
|
|
1291
1896
|
var results: [[String: Any]] = []
|
|
1292
|
-
let dispatchGroup = DispatchGroup()
|
|
1293
1897
|
let resultsLock = NSLock()
|
|
1294
1898
|
|
|
1899
|
+
// 한 장씩 순서대로 처리 (동시에 전부 디코딩하면 메모리가 치솟음). 해상도는 줄이지 않음
|
|
1295
1900
|
for (index, asset) in assets.enumerated() {
|
|
1901
|
+
autoreleasepool {
|
|
1902
|
+
let dispatchGroup = DispatchGroup()
|
|
1296
1903
|
dispatchGroup.enter()
|
|
1297
1904
|
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
1298
1905
|
let originalUri = "ph://\(asset.localIdentifier)"
|
|
@@ -1367,10 +1974,10 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1367
1974
|
}
|
|
1368
1975
|
}
|
|
1369
1976
|
}
|
|
1977
|
+
dispatchGroup.wait()
|
|
1978
|
+
} // end autoreleasepool
|
|
1370
1979
|
}
|
|
1371
1980
|
|
|
1372
|
-
dispatchGroup.wait()
|
|
1373
|
-
|
|
1374
1981
|
DispatchQueue.main.async {
|
|
1375
1982
|
self.currentGalleryVC = nil
|
|
1376
1983
|
if results.isEmpty {
|
|
@@ -1379,6 +1986,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1379
1986
|
// 순서를 유지하기 위해
|
|
1380
1987
|
resolve(results)
|
|
1381
1988
|
}
|
|
1989
|
+
galleryVC.releaseAfterReturn()
|
|
1382
1990
|
}
|
|
1383
1991
|
}
|
|
1384
1992
|
}
|
|
@@ -1625,6 +2233,7 @@ fileprivate extension RNAttachmentKit {
|
|
|
1625
2233
|
|
|
1626
2234
|
editorVC.onEditDeleted = { [weak currentGallery] asset in
|
|
1627
2235
|
currentGallery?.editedImages.removeValue(forKey: asset.localIdentifier)
|
|
2236
|
+
currentGallery?.videoEditStates.removeValue(forKey: asset.localIdentifier) // 동영상 편집도 기본으로 돌아간 것
|
|
1628
2237
|
}
|
|
1629
2238
|
|
|
1630
2239
|
editorVC.modalPresentationStyle = .overFullScreen
|
|
@@ -1640,7 +2249,22 @@ fileprivate extension RNAttachmentKit {
|
|
|
1640
2249
|
}
|
|
1641
2250
|
}
|
|
1642
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
|
+
}
|
|
1643
2262
|
|
|
2263
|
+
if sourceFrame == .zero {
|
|
2264
|
+
// 기준 칸이 없을 때(칸이 화면 밖 등): 기본 전환(아래로 미끄러짐)은 끌어내려 닫을 때 위로 치워 둔 상단 바가
|
|
2265
|
+
// 화면과 함께 내려오며 보이므로 페이드로
|
|
2266
|
+
editorVC.modalTransitionStyle = .crossDissolve
|
|
2267
|
+
}
|
|
1644
2268
|
if sourceFrame != .zero {
|
|
1645
2269
|
self.editorTransitionDelegate = ImageEditorTransitionDelegate()
|
|
1646
2270
|
self.editorTransitionDelegate?.sourceFrame = sourceFrame
|
|
@@ -1674,10 +2298,16 @@ fileprivate extension RNAttachmentKit {
|
|
|
1674
2298
|
self.editorTransitionDelegate?.onCurrentAssetChanged = { [weak currentGallery] currentAsset in
|
|
1675
2299
|
currentGallery?.setHiddenAsset(currentAsset, scrollIntoView: true)
|
|
1676
2300
|
}
|
|
1677
|
-
self.editorTransitionDelegate?.onEditorClosed = { [weak currentGallery] in
|
|
2301
|
+
self.editorTransitionDelegate?.onEditorClosed = { [weak self, weak currentGallery] in
|
|
1678
2302
|
currentGallery?.setHiddenAsset(nil)
|
|
2303
|
+
// 편집 화면이 닫혔으니 sourceImage/uncroppedImage 를 들고 있는 전환 델리게이트를 놓아 줌
|
|
2304
|
+
// (호출 중인 객체이므로 다음 런루프에서)
|
|
2305
|
+
DispatchQueue.main.async { self?.editorTransitionDelegate = nil }
|
|
2306
|
+
}
|
|
2307
|
+
// 칸은 열기 애니메이션의 첫 프레임에 비움 (누르자마자 비우면 편집 화면이 준비되는 동안 빈 칸이 번쩍임)
|
|
2308
|
+
self.editorTransitionDelegate?.onPresentationStarted = { [weak currentGallery] in
|
|
2309
|
+
currentGallery?.setHiddenAsset(asset)
|
|
1679
2310
|
}
|
|
1680
|
-
currentGallery.setHiddenAsset(asset)
|
|
1681
2311
|
|
|
1682
2312
|
editorVC.transitioningDelegate = self.editorTransitionDelegate
|
|
1683
2313
|
}
|
|
@@ -1686,10 +2316,10 @@ fileprivate extension RNAttachmentKit {
|
|
|
1686
2316
|
editorVC.themeColor = color
|
|
1687
2317
|
}
|
|
1688
2318
|
|
|
1689
|
-
editorVC.onConfirmMulti = { [weak currentGallery] (assets, croppedImages, filterStates) in
|
|
2319
|
+
editorVC.onConfirmMulti = { [weak currentGallery, weak editorVC] (assets, croppedImages, filterStates) in
|
|
1690
2320
|
guard let currentGallery = currentGallery else { return }
|
|
1691
2321
|
|
|
1692
|
-
editorVC
|
|
2322
|
+
editorVC?.dismiss(animated: true) {
|
|
1693
2323
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
1694
2324
|
var newlyEditedAssets: [PHAsset] = []
|
|
1695
2325
|
|
|
@@ -1731,7 +2361,7 @@ fileprivate extension RNAttachmentKit {
|
|
|
1731
2361
|
}
|
|
1732
2362
|
|
|
1733
2363
|
// patch398: 편집 화면의 전송 버튼 — 편집본을 갤러리에 반영하고 선택한 사진을 바로 전송(편집 화면/갤러리 모두 닫힘)
|
|
1734
|
-
editorVC.onSendMulti = { [weak currentGallery] (assets, croppedImages, filterStates) in
|
|
2364
|
+
editorVC.onSendMulti = { [weak currentGallery, weak editorVC] (assets, croppedImages, filterStates) in
|
|
1735
2365
|
guard let currentGallery = currentGallery else { return }
|
|
1736
2366
|
for asset in assets {
|
|
1737
2367
|
guard var img = croppedImages[asset] else { continue }
|
|
@@ -1748,7 +2378,7 @@ fileprivate extension RNAttachmentKit {
|
|
|
1748
2378
|
// patch399: 편집 화면의 이미지가 갤러리 자리로 돌아가는 애니메이션이 끝난 뒤에 전송합니다.
|
|
1749
2379
|
currentGallery.refreshThumbnails(for: assets)
|
|
1750
2380
|
currentGallery.updateSelectedCellNumbers()
|
|
1751
|
-
editorVC
|
|
2381
|
+
editorVC?.dismiss(animated: true) { [weak currentGallery] in
|
|
1752
2382
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
1753
2383
|
currentGallery?.sendSelectionFromEditor()
|
|
1754
2384
|
}
|
|
@@ -1801,3 +2431,69 @@ extension RNAttachmentKit: UIViewControllerTransitioningDelegate {
|
|
|
1801
2431
|
return zoomAnimator
|
|
1802
2432
|
}
|
|
1803
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
|
+
}
|