react-native-attachment-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +327 -0
- package/android/CMakeLists.txt +31 -0
- package/android/build.gradle +121 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
- package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +1159 -0
- package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitPackage.kt +37 -0
- package/android/src/oldarch/java/com/rnattachmentkit/NativeRNAttachmentKitSpec.kt +19 -0
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +1142 -0
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitPackage.kt +19 -0
- package/ios/README.md +213 -0
- package/ios/RNAttachmentKit.m +132 -0
- package/ios/RNAttachmentKit.swift +1787 -0
- package/ios/RNAttachmentKit.xcframework/Info.plist +44 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Base.lproj/LaunchScreen.storyboardc/Info.plist +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Base.lproj/LaunchScreen.storyboardc/UIViewController-01J-lp-oVM.nib +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Headers/RNAttachmentKit-Swift.h +581 -0
- 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 +34376 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +517 -0
- 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 +517 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/module.modulemap +3 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Pretendard-SemiBold.otf +0 -0
- 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_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/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeRequirements +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +498 -0
- 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/Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Base.lproj/LaunchScreen.storyboardc/Info.plist +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Base.lproj/LaunchScreen.storyboardc/UIViewController-01J-lp-oVM.nib +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Headers/RNAttachmentKit-Swift.h +1158 -0
- 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 +34376 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +517 -0
- 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 +517 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +34376 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +517 -0
- 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 +517 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/module.modulemap +3 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Pretendard-SemiBold.otf +0 -0
- 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_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/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeRequirements +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +553 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/Contents.json +6 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_back.imageset/Contents.json +24 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_back.imageset/ic_back_dark.png +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_back.imageset/ic_back_normal.png +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_delete.imageset/Contents.json +56 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_delete.imageset/delete_dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_delete.imageset/delete_normal.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_forward.imageset/Contents.json +56 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_forward.imageset/forward_dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_forward.imageset/forward_normal.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_reward.imageset/Contents.json +56 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_reward.imageset/reward_dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_reward.imageset/reward_normal.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_crop.imageset/Contents.json +52 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_crop.imageset/crop_dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_crop.imageset/crop_normal.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_drawing.imageset/Contents.json +52 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_drawing.imageset/drawing_dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_drawing.imageset/drawing_normal.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_effect.imageset/Contents.json +52 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_effect.imageset/effect_dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_effect.imageset/effect_normal.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_emoji.imageset/Contents.json +52 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_emoji.imageset/emoji_dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_emoji.imageset/emoji_normal.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_text.imageset/Contents.json +52 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_text.imageset/text_dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_text.imageset/text_norml.webp +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/rotate_icon.imageset/Contents.json +32 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/rotate_icon.imageset/rotate_dark.png +0 -0
- package/ios/RNAttachmentKitAssets/Assets.xcassets/rotate_icon.imageset/rotate_normal.png +0 -0
- package/ios/RNAttachmentKitAssets/Fonts/Pretendard-SemiBold.otf +0 -0
- 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_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/package.json +68 -0
- package/react-native-attachment-kit.podspec +51 -0
- package/react-native.config.js +14 -0
- package/src/NativeAttachmentKit.ts +26 -0
- package/src/index.d.ts +56 -0
- package/src/index.ts +359 -0
- package/src/types.ts +317 -0
|
@@ -0,0 +1,1787 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RNAttachmentKit.swift
|
|
3
|
+
// RNAttachmentKit
|
|
4
|
+
//
|
|
5
|
+
// Supports both Turbo Module (New Architecture) and Legacy Native Module
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import UIKit
|
|
9
|
+
import Photos
|
|
10
|
+
import ImageIO
|
|
11
|
+
import RNAttachmentKit
|
|
12
|
+
|
|
13
|
+
#if RCT_NEW_ARCH_ENABLED
|
|
14
|
+
import React
|
|
15
|
+
#else
|
|
16
|
+
import React
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
#if canImport(SDWebImageWebPCoder)
|
|
20
|
+
import SDWebImageWebPCoder
|
|
21
|
+
#endif
|
|
22
|
+
|
|
23
|
+
@objc(RNAttachmentKit)
|
|
24
|
+
class RNAttachmentKit: RCTEventEmitter {
|
|
25
|
+
|
|
26
|
+
public static var shared: RNAttachmentKit?
|
|
27
|
+
|
|
28
|
+
private var zoomAnimator = ZoomTransitionAnimator()
|
|
29
|
+
private weak var activeViewerVC: RemoteImageViewerViewController?
|
|
30
|
+
|
|
31
|
+
override init() {
|
|
32
|
+
super.init()
|
|
33
|
+
RNAttachmentKit.shared = self
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 현재 표시 중인 갤러리 참조
|
|
37
|
+
private weak var currentGalleryVC: GalleryViewController?
|
|
38
|
+
private var editorTransitionDelegate: ImageEditorTransitionDelegate?
|
|
39
|
+
/// 열려 있는 미디어 갤러리 (galleryId → 화면)
|
|
40
|
+
fileprivate var mediaGalleries: [String: MediaGalleryViewController] = [:]
|
|
41
|
+
|
|
42
|
+
// MARK: - Module Setup
|
|
43
|
+
|
|
44
|
+
@objc
|
|
45
|
+
override static func moduleName() -> String! {
|
|
46
|
+
return "RNAttachmentKit"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@objc
|
|
50
|
+
override static func requiresMainQueueSetup() -> Bool {
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@objc
|
|
55
|
+
override func supportedEvents() -> [String]! {
|
|
56
|
+
return ["onSelectionChange", "onImageProcessed", "onPageSelected", "onViewerWillClose", "onImageDeleted",
|
|
57
|
+
"onGalleryItemDeleted", "onGalleryNeedPage", "onGalleryClosed", "onGalleryGridSizeChanged"]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// MARK: - Public Methods
|
|
61
|
+
|
|
62
|
+
static let defaultAnimationConfig: [String: Double] = [
|
|
63
|
+
"galleryOpen": 300.0,
|
|
64
|
+
"galleryClose": 250.0,
|
|
65
|
+
"editorOpen": 350.0,
|
|
66
|
+
"editorClose": 350.0,
|
|
67
|
+
"viewerOpen": 370.0,
|
|
68
|
+
"viewerClose": 370.0
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
@objc
|
|
72
|
+
func getDefaultAnimationConfig(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
73
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
74
|
+
resolve(RNAttachmentKit.defaultAnimationConfig)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@objc
|
|
79
|
+
func `init`(_ licenseKey: String,
|
|
80
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
81
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
82
|
+
let result = LicenseManager.shared.initialize(with: licenseKey)
|
|
83
|
+
resolve(result)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MARK: - Public Methods
|
|
87
|
+
|
|
88
|
+
@objc
|
|
89
|
+
func injectImageCache(_ urlString: String, localPath: String,
|
|
90
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
91
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
92
|
+
if ViewerImageCache.shared.hasCache(for: urlString) {
|
|
93
|
+
resolve(true)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
98
|
+
// localPath might be a file:// URI or raw path
|
|
99
|
+
var path = localPath
|
|
100
|
+
if path.hasPrefix("file://") {
|
|
101
|
+
path = path.replacingOccurrences(of: "file://", with: "")
|
|
102
|
+
}
|
|
103
|
+
if let image = UIImage(contentsOfFile: path) {
|
|
104
|
+
if #available(iOS 15.0, *) {
|
|
105
|
+
image.prepareForDisplay { preparedImage in
|
|
106
|
+
let finalImage = preparedImage ?? image
|
|
107
|
+
ViewerImageCache.shared.saveImage(finalImage, for: urlString)
|
|
108
|
+
resolve(true)
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
|
|
112
|
+
image.draw(at: .zero)
|
|
113
|
+
let decodedImage = UIGraphicsGetImageFromCurrentImageContext() ?? image
|
|
114
|
+
UIGraphicsEndImageContext()
|
|
115
|
+
ViewerImageCache.shared.saveImage(decodedImage, for: urlString)
|
|
116
|
+
resolve(true)
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
reject("ERROR", "Failed to load image from local path", nil)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@objc
|
|
125
|
+
func updateSourceRect(_ options: NSDictionary,
|
|
126
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
127
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
128
|
+
let optDict = options as? [String: Any] ?? [:]
|
|
129
|
+
let x = (optDict["x"] as? NSNumber).map { CGFloat(truncating: $0) } ?? (optDict["x"] as? Double).map { CGFloat($0) }
|
|
130
|
+
let y = (optDict["y"] as? NSNumber).map { CGFloat(truncating: $0) } ?? (optDict["y"] as? Double).map { CGFloat($0) }
|
|
131
|
+
let w = (optDict["width"] as? NSNumber).map { CGFloat(truncating: $0) } ?? (optDict["width"] as? Double).map { CGFloat($0) }
|
|
132
|
+
let h = (optDict["height"] as? NSNumber).map { CGFloat(truncating: $0) } ?? (optDict["height"] as? Double).map { CGFloat($0) }
|
|
133
|
+
|
|
134
|
+
if let x = x, let y = y, let w = w, let h = h {
|
|
135
|
+
DispatchQueue.main.async {
|
|
136
|
+
self.zoomAnimator.sourceRect = CGRect(x: x, y: y, width: w, height: h)
|
|
137
|
+
|
|
138
|
+
var mask: CACornerMask = []
|
|
139
|
+
if let corners = optDict["sourceBorderCorners"] as? [String] {
|
|
140
|
+
if corners.contains("topLeft") { mask.insert(.layerMinXMinYCorner) }
|
|
141
|
+
if corners.contains("topRight") { mask.insert(.layerMaxXMinYCorner) }
|
|
142
|
+
if corners.contains("bottomLeft") { mask.insert(.layerMinXMaxYCorner) }
|
|
143
|
+
if corners.contains("bottomRight") { mask.insert(.layerMaxXMaxYCorner) }
|
|
144
|
+
} else {
|
|
145
|
+
mask = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
|
146
|
+
}
|
|
147
|
+
self.zoomAnimator.sourceBorderCorners = mask
|
|
148
|
+
// 넘긴 사진 자리의 라운드 값도 갱신 (예: 원형 대표 사진 → 둥근 사각 카드).
|
|
149
|
+
// 이전에는 처음 연 사진의 라운드가 그대로 남아, 다른 모양의 자리로 닫힐 때 모양이 틀어졌음.
|
|
150
|
+
if let r = optDict["sourceBorderRadius"] as? NSNumber {
|
|
151
|
+
self.zoomAnimator.sourceBorderRadius = CGFloat(truncating: r)
|
|
152
|
+
} else if let r = optDict["sourceBorderRadius"] as? Double {
|
|
153
|
+
self.zoomAnimator.sourceBorderRadius = CGFloat(r)
|
|
154
|
+
}
|
|
155
|
+
self.zoomAnimator.updateMaskView()
|
|
156
|
+
|
|
157
|
+
resolve(true)
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
reject("ERROR", "Invalid coordinates", nil)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@objc
|
|
165
|
+
func openViewer(_ options: NSDictionary,
|
|
166
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
167
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
168
|
+
guard let images = options["images"] as? [String] else {
|
|
169
|
+
reject("ERROR", "images array is required", nil)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let initialIndex = options["initialIndex"] as? Int ?? 0
|
|
174
|
+
let placeholderImages = options["placeholderImages"] as? [String]
|
|
175
|
+
// cacheFiles: images 와 같은 순서의 로컬 캐시 파일 경로 (null 항목은 NSNull 로 옴 → 빈 문자열)
|
|
176
|
+
let cacheFiles: [String]? = (options["cacheFiles"] as? [Any])?.map { ($0 as? String) ?? "" }
|
|
177
|
+
let themeColorHex = options["themeColor"] as? String
|
|
178
|
+
let languageCode = options["languageCode"] as? String ?? "en"
|
|
179
|
+
let viewerTitle = options["title"] as? String
|
|
180
|
+
let animationType = options["animationType"] as? String ?? (options["sourceRect"] != nil ? "zoom" : "slide")
|
|
181
|
+
let closeAnimationType = options["closeAnimationType"] as? String
|
|
182
|
+
// 저장/공유/삭제 상단바 버튼 - 사용자가 준 스크린샷(전체 저장/이 사진만 저장 등)에
|
|
183
|
+
// 맞춘 기본 한국어 문구가 있으므로, JS에서 안 넘기면 그 기본값을 그대로 씁니다.
|
|
184
|
+
let showActionButtons = (options["showActionButtons"] as? Bool) ?? true
|
|
185
|
+
let saveAllText = options["saveAllText"] as? String
|
|
186
|
+
let saveOneText = options["saveOneText"] as? String
|
|
187
|
+
let shareAllText = options["shareAllText"] as? String
|
|
188
|
+
let shareOneText = options["shareOneText"] as? String
|
|
189
|
+
let actionCancelText = options["actionCancelText"] as? String
|
|
190
|
+
let deleteConfirmTitle = options["deleteConfirmTitle"] as? String
|
|
191
|
+
let deleteConfirmMessage = options["deleteConfirmMessage"] as? String
|
|
192
|
+
let deleteConfirmGroupMessage = options["deleteConfirmGroupMessage"] as? String
|
|
193
|
+
let deleteButtonText = options["deleteButtonText"] as? String
|
|
194
|
+
// patch405: 삭제 방식 선택지 [{ key, label, destructive }]
|
|
195
|
+
let deleteOptions: [RemoteImageViewerViewController.DeleteOption] =
|
|
196
|
+
((options["deleteOptions"] as? [Any]) ?? []).compactMap { raw in
|
|
197
|
+
guard let d = raw as? [String: Any],
|
|
198
|
+
let key = d["key"] as? String, !key.isEmpty,
|
|
199
|
+
let label = d["label"] as? String, !label.isEmpty else { return nil }
|
|
200
|
+
let destructive = (d["destructive"] as? Bool) ?? ((d["destructive"] as? NSNumber)?.boolValue ?? false)
|
|
201
|
+
return RemoteImageViewerViewController.DeleteOption(key: key, label: label, destructive: destructive)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
var sourceBackgroundColor: UIColor = .clear
|
|
205
|
+
if let bgString = options["sourceBackgroundColorString"] as? String {
|
|
206
|
+
let comps = bgString.split(separator: ",")
|
|
207
|
+
if comps.count >= 3,
|
|
208
|
+
let r = Double(comps[0].trimmingCharacters(in: .whitespaces)),
|
|
209
|
+
let g = Double(comps[1].trimmingCharacters(in: .whitespaces)),
|
|
210
|
+
let b = Double(comps[2].trimmingCharacters(in: .whitespaces)) {
|
|
211
|
+
sourceBackgroundColor = UIColor(red: CGFloat(r/255.0), green: CGFloat(g/255.0), blue: CGFloat(b/255.0), alpha: 1.0)
|
|
212
|
+
}
|
|
213
|
+
} else if let bgHex = options["sourceBackgroundColor"] as? String, let color = UIColor(hexString: bgHex) {
|
|
214
|
+
sourceBackgroundColor = color
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
var parsedRect: CGRect?
|
|
218
|
+
if let sourceRectDict = options["sourceRect"] as? NSDictionary {
|
|
219
|
+
var x: CGFloat = 0
|
|
220
|
+
var y: CGFloat = 0
|
|
221
|
+
var w: CGFloat = 0
|
|
222
|
+
var h: CGFloat = 0
|
|
223
|
+
if let xNum = sourceRectDict["x"] as? NSNumber { x = CGFloat(truncating: xNum) } else if let xDbl = sourceRectDict["x"] as? Double { x = CGFloat(xDbl) }
|
|
224
|
+
if let yNum = sourceRectDict["y"] as? NSNumber { y = CGFloat(truncating: yNum) } else if let yDbl = sourceRectDict["y"] as? Double { y = CGFloat(yDbl) }
|
|
225
|
+
if let wNum = sourceRectDict["width"] as? NSNumber { w = CGFloat(truncating: wNum) } else if let wDbl = sourceRectDict["width"] as? Double { w = CGFloat(wDbl) }
|
|
226
|
+
if let hNum = sourceRectDict["height"] as? NSNumber { h = CGFloat(truncating: hNum) } else if let hDbl = sourceRectDict["height"] as? Double { h = CGFloat(hDbl) }
|
|
227
|
+
parsedRect = CGRect(x: x, y: y, width: w, height: h)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
var sourceBorderRadius: CGFloat = 0
|
|
231
|
+
if let radiusNum = options["sourceBorderRadius"] as? NSNumber {
|
|
232
|
+
sourceBorderRadius = CGFloat(truncating: radiusNum)
|
|
233
|
+
} else if let radiusDbl = options["sourceBorderRadius"] as? Double {
|
|
234
|
+
sourceBorderRadius = CGFloat(radiusDbl)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
var mask: CACornerMask = []
|
|
238
|
+
if let corners = options["sourceBorderCorners"] as? [String] {
|
|
239
|
+
if corners.contains("topLeft") { mask.insert(.layerMinXMinYCorner) }
|
|
240
|
+
if corners.contains("topRight") { mask.insert(.layerMaxXMinYCorner) }
|
|
241
|
+
if corners.contains("bottomLeft") { mask.insert(.layerMinXMaxYCorner) }
|
|
242
|
+
if corners.contains("bottomRight") { mask.insert(.layerMaxXMaxYCorner) }
|
|
243
|
+
// Empty array means no rounded corners (e.g. interior group image cell)
|
|
244
|
+
} else {
|
|
245
|
+
mask = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
DispatchQueue.main.async {
|
|
249
|
+
guard let rootViewController = self.getRootViewController() else {
|
|
250
|
+
let error = NSError(domain: "RNAttachmentKit", code: 0, userInfo: nil)
|
|
251
|
+
reject("ERROR", "Unable to get root view controller", error)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let viewerVC = RemoteImageViewerViewController(imageUrls: images, initialIndex: initialIndex)
|
|
256
|
+
viewerVC.languageCode = languageCode
|
|
257
|
+
viewerVC.viewerTitle = viewerTitle
|
|
258
|
+
viewerVC.placeholderImages = placeholderImages
|
|
259
|
+
viewerVC.cacheFiles = cacheFiles
|
|
260
|
+
if let hex = themeColorHex, let color = UIColor(hexString: hex) {
|
|
261
|
+
viewerVC.themeColor = color
|
|
262
|
+
}
|
|
263
|
+
viewerVC.showActionButtons = showActionButtons
|
|
264
|
+
if let saveAllText = saveAllText { viewerVC.saveAllText = saveAllText }
|
|
265
|
+
if let saveOneText = saveOneText { viewerVC.saveOneText = saveOneText }
|
|
266
|
+
if let shareAllText = shareAllText { viewerVC.shareAllText = shareAllText }
|
|
267
|
+
if let shareOneText = shareOneText { viewerVC.shareOneText = shareOneText }
|
|
268
|
+
if let actionCancelText = actionCancelText { viewerVC.actionCancelText = actionCancelText }
|
|
269
|
+
if let deleteConfirmTitle = deleteConfirmTitle { viewerVC.deleteConfirmTitle = deleteConfirmTitle }
|
|
270
|
+
viewerVC.deleteConfirmMessage = deleteConfirmMessage
|
|
271
|
+
viewerVC.deleteConfirmGroupMessage = deleteConfirmGroupMessage
|
|
272
|
+
if let deleteButtonText = deleteButtonText { viewerVC.deleteButtonText = deleteButtonText }
|
|
273
|
+
viewerVC.deleteOptions = deleteOptions
|
|
274
|
+
|
|
275
|
+
self.zoomAnimator.animationType = animationType
|
|
276
|
+
self.zoomAnimator.closeAnimationType = closeAnimationType
|
|
277
|
+
if let rect = parsedRect {
|
|
278
|
+
self.zoomAnimator.sourceRect = rect
|
|
279
|
+
}
|
|
280
|
+
self.zoomAnimator.sourceBorderRadius = sourceBorderRadius
|
|
281
|
+
self.zoomAnimator.sourceBorderCorners = mask
|
|
282
|
+
self.zoomAnimator.sourceBackgroundColor = sourceBackgroundColor
|
|
283
|
+
// Reset any sourceImage left over from a previous openViewer() call - it's only
|
|
284
|
+
// meant to carry this call's local placeholder (see below), and must not leak
|
|
285
|
+
// into a later call that has no placeholder of its own.
|
|
286
|
+
self.zoomAnimator.sourceImage = nil
|
|
287
|
+
|
|
288
|
+
viewerVC.modalPresentationStyle = .custom
|
|
289
|
+
viewerVC.transitioningDelegate = self
|
|
290
|
+
|
|
291
|
+
self.activeViewerVC = viewerVC
|
|
292
|
+
viewerVC.onPageChanged = { [weak self] (index: Int) -> Void in
|
|
293
|
+
guard let self = self else { return }
|
|
294
|
+
let eventBody: [String: Any] = ["index": index]
|
|
295
|
+
self.sendEvent(withName: "onPageSelected", body: eventBody)
|
|
296
|
+
}
|
|
297
|
+
// ⚠️ 이전에는 이 클로저를 전혀 연결하지 않아서 "onViewerWillClose" 네이티브 이벤트가
|
|
298
|
+
// iOS에서는 절대 발생하지 않았습니다. JS 쪽(src/index.ts)은 정확히 이 이벤트가 와야만
|
|
299
|
+
// "onPageSelected" 구독을 정리(subscriptionPageSelected.remove())하기 때문에, 뷰어를
|
|
300
|
+
// 열 때마다 그 구독이 영구히 누수됐습니다. onPageSelected는 뷰어별로 구분되지 않는
|
|
301
|
+
// 전역 이벤트라서, 그룹 이미지를 여러 번 열면 예전 메시지의 리스너까지 전부 같이
|
|
302
|
+
// 반응해서 서로 다른 메시지의 measureMap 기준으로 updateSourceRect를 호출해
|
|
303
|
+
// 공유된 zoomAnimator.sourceRect를 덮어써버렸습니다 - "스와이프 후 닫으면 전혀
|
|
304
|
+
// 다른 그룹 이미지 위치로 애니메이션되는" 버그의 근본 원인이었습니다. 이제 실제로
|
|
305
|
+
// 뷰어가 닫히기 시작할 때 이벤트를 보내서 JS가 제때 구독을 정리하도록 합니다.
|
|
306
|
+
viewerVC.onViewerWillClose = { [weak self] () -> Void in
|
|
307
|
+
self?.sendEvent(withName: "onViewerWillClose", body: nil)
|
|
308
|
+
}
|
|
309
|
+
viewerVC.onImageDeleted = { [weak self] (index: Int, url: String, option: String?) -> Void in
|
|
310
|
+
guard let self = self else { return }
|
|
311
|
+
var eventBody: [String: Any] = ["index": index, "url": url]
|
|
312
|
+
if let option = option { eventBody["option"] = option } // patch405
|
|
313
|
+
self.sendEvent(withName: "onImageDeleted", body: eventBody)
|
|
314
|
+
}
|
|
315
|
+
viewerVC.onWillDismissForDelete = { [weak self] () -> Void in
|
|
316
|
+
self?.zoomAnimator.fadeOutOnDismiss = true
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let resolveResult: [String: Any] = ["success": true]
|
|
320
|
+
|
|
321
|
+
// Cache-first, then local placeholder, then wait-for-download - in that priority
|
|
322
|
+
// order. If the full-resolution image is ALREADY cached (memory or disk), we must
|
|
323
|
+
// animate with THAT, not with a lower-quality local thumbnail placeholder: using the
|
|
324
|
+
// placeholder here would show a needless blurry-then-sharp flash for an image that's
|
|
325
|
+
// already available in full quality. The placeholder is only a stand-in for when the
|
|
326
|
+
// full image genuinely isn't available yet (mirrors Android's onlyRetrieveFromCache
|
|
327
|
+
// gating - "use the real image if it's already cached, otherwise the local thumbnail").
|
|
328
|
+
// NOTE: only the memory cache is checked here (not the disk cache) - this bridge
|
|
329
|
+
// file is compiled directly as a pod source file, a SEPARATE module from the
|
|
330
|
+
// ViewerImageCache class (which ships compiled inside RNAttachmentKit.xcframework).
|
|
331
|
+
// Cross-module Swift visibility requires `public`, and getDiskImageSynchronously()
|
|
332
|
+
// wasn't public until this same fix - until the xcframework is rebuilt (deploy.rb /
|
|
333
|
+
// build_xcframework.sh) and re-synced to Dist, calling it here would fail to compile.
|
|
334
|
+
// Memory-cache is the common case anyway (an image just viewed/downloaded this
|
|
335
|
+
// session); a disk-only cache hit will fall through to the placeholder path below,
|
|
336
|
+
// which is still correct (just not maximally optimal) until that rebuild happens.
|
|
337
|
+
var cachedFullImage: UIImage? = nil
|
|
338
|
+
if images.indices.contains(initialIndex) {
|
|
339
|
+
let urlString = images[initialIndex]
|
|
340
|
+
cachedFullImage = ViewerImageCache.shared.getMemoryImage(for: urlString)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
var initialPlaceholderImage: UIImage? = nil
|
|
344
|
+
if cachedFullImage == nil, let placeholderImages = placeholderImages, placeholderImages.indices.contains(initialIndex) {
|
|
345
|
+
let placeholderUrlString = placeholderImages[initialIndex]
|
|
346
|
+
if placeholderUrlString.hasPrefix("file://"), let url = URL(string: placeholderUrlString) {
|
|
347
|
+
initialPlaceholderImage = UIImage(contentsOfFile: url.path)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if let cachedImage = cachedFullImage {
|
|
352
|
+
self.zoomAnimator.sourceImage = cachedImage
|
|
353
|
+
rootViewController.present(viewerVC, animated: true, completion: {
|
|
354
|
+
resolve(resolveResult)
|
|
355
|
+
})
|
|
356
|
+
} else if let placeholderImage = initialPlaceholderImage {
|
|
357
|
+
self.zoomAnimator.sourceImage = placeholderImage
|
|
358
|
+
rootViewController.present(viewerVC, animated: true, completion: {
|
|
359
|
+
resolve(resolveResult)
|
|
360
|
+
})
|
|
361
|
+
if images.indices.contains(initialIndex) {
|
|
362
|
+
// Kick off/continue the full-resolution fetch in the background so it's
|
|
363
|
+
// ready (and cached) as soon as possible; the viewer's own page loading
|
|
364
|
+
// (ZoomableImageItemView.loadImage) will pick it up and swap it in.
|
|
365
|
+
RemoteImageViewerViewController.prefetchImage(from: images[initialIndex], completion: { _ in })
|
|
366
|
+
}
|
|
367
|
+
} else if images.indices.contains(initialIndex) {
|
|
368
|
+
// No local placeholder available (exceptional case) - fall back to the previous
|
|
369
|
+
// behavior: present immediately if already cached, otherwise wait for the
|
|
370
|
+
// download to complete since there's nothing to animate with yet.
|
|
371
|
+
let initialUrl = images[initialIndex]
|
|
372
|
+
RemoteImageViewerViewController.prefetchImage(from: initialUrl) { _ in
|
|
373
|
+
DispatchQueue.main.async {
|
|
374
|
+
rootViewController.present(viewerVC, animated: true, completion: {
|
|
375
|
+
resolve(resolveResult)
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
rootViewController.present(viewerVC, animated: true, completion: {
|
|
381
|
+
resolve(resolveResult)
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@objc
|
|
388
|
+
func updateViewerSourceRect(_ rect: NSDictionary,
|
|
389
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
390
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
391
|
+
DispatchQueue.main.async {
|
|
392
|
+
let x = (rect["x"] as? NSNumber).map { CGFloat(truncating: $0) } ?? (rect["x"] as? Double).map { CGFloat($0) } ?? 0
|
|
393
|
+
let y = (rect["y"] as? NSNumber).map { CGFloat(truncating: $0) } ?? (rect["y"] as? Double).map { CGFloat($0) } ?? 0
|
|
394
|
+
let w = (rect["width"] as? NSNumber).map { CGFloat(truncating: $0) } ?? (rect["width"] as? Double).map { CGFloat($0) } ?? 0
|
|
395
|
+
let h = (rect["height"] as? NSNumber).map { CGFloat(truncating: $0) } ?? (rect["height"] as? Double).map { CGFloat($0) } ?? 0
|
|
396
|
+
|
|
397
|
+
var mask: CACornerMask = []
|
|
398
|
+
if let corners = rect["sourceBorderCorners"] as? [String] {
|
|
399
|
+
if corners.contains("topLeft") { mask.insert(.layerMinXMinYCorner) }
|
|
400
|
+
if corners.contains("topRight") { mask.insert(.layerMaxXMinYCorner) }
|
|
401
|
+
if corners.contains("bottomLeft") { mask.insert(.layerMinXMaxYCorner) }
|
|
402
|
+
if corners.contains("bottomRight") { mask.insert(.layerMaxXMaxYCorner) }
|
|
403
|
+
if mask.isEmpty { mask = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] }
|
|
404
|
+
} else {
|
|
405
|
+
mask = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
self.zoomAnimator.sourceRect = CGRect(x: x, y: y, width: w, height: h)
|
|
409
|
+
self.zoomAnimator.sourceBorderCorners = mask
|
|
410
|
+
self.zoomAnimator.updateMaskView()
|
|
411
|
+
resolve(nil)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
@objc
|
|
416
|
+
func openEditor(_ options: NSDictionary,
|
|
417
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
418
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
419
|
+
guard let uri = options["uri"] as? String else {
|
|
420
|
+
reject("ERROR", "uri is required", nil)
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
var themeColor: UIColor? = nil
|
|
425
|
+
if let themeColorHex = options["themeColor"] as? String {
|
|
426
|
+
themeColor = UIColor(hexString: themeColorHex)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
var parsedMaxWidth: Int? = nil
|
|
430
|
+
var parsedMaxHeight: Int? = nil
|
|
431
|
+
if let maxW = options["maxWidth"] as? Int { parsedMaxWidth = maxW }
|
|
432
|
+
if let maxH = options["maxHeight"] as? Int { parsedMaxHeight = maxH }
|
|
433
|
+
|
|
434
|
+
DispatchQueue.main.async {
|
|
435
|
+
guard let rootViewController = self.getRootViewController() else {
|
|
436
|
+
reject("ERROR", "Unable to get root view controller", nil)
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
if uri.hasPrefix("ph://") {
|
|
442
|
+
let localIdentifier = uri.replacingOccurrences(of: "ph://", with: "")
|
|
443
|
+
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil)
|
|
444
|
+
|
|
445
|
+
guard let asset = fetchResult.firstObject else {
|
|
446
|
+
reject("ERROR", "Asset not found", nil)
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let editorVC = ImageEditorViewController()
|
|
451
|
+
editorVC.languageCode = options["languageCode"] as? String ?? "en"
|
|
452
|
+
editorVC.allAssets = [asset]
|
|
453
|
+
editorVC.currentIndex = 0
|
|
454
|
+
editorVC.modalPresentationStyle = .fullScreen
|
|
455
|
+
editorVC.singlePhotoMode = true
|
|
456
|
+
if let color = themeColor {
|
|
457
|
+
editorVC.themeColor = color
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if let editedUriStr = options["editedFileUri"] as? String,
|
|
461
|
+
let url = URL(string: editedUriStr) ?? URL(string: "file://" + editedUriStr.replacingOccurrences(of: "file://", with: "")),
|
|
462
|
+
let imgData = try? Data(contentsOf: url),
|
|
463
|
+
let img = UIImage(data: imgData) {
|
|
464
|
+
editorVC.croppedImages[0] = img
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
editorVC.onConfirm = { [weak self, weak editorVC] (editedAsset, editedImage, filterState, caption) in
|
|
468
|
+
editorVC?.dismiss(animated: true) {
|
|
469
|
+
self?.processEditorResult(asset: editedAsset, croppedImage: editedImage, filterState: filterState, caption: caption, galleryVC: nil, targetMaxWidth: parsedMaxWidth, targetMaxHeight: parsedMaxHeight, resolve: { resultArr in
|
|
470
|
+
if let arr = resultArr as? [[String: Any]], let first = arr.first {
|
|
471
|
+
resolve(first)
|
|
472
|
+
} else {
|
|
473
|
+
resolve(resultArr)
|
|
474
|
+
}
|
|
475
|
+
}, reject: reject)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
editorVC.onCancel = { [weak editorVC] in
|
|
480
|
+
editorVC?.dismiss(animated: true) {
|
|
481
|
+
reject("CANCELLED", "User cancelled editor", nil)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
var topController = rootViewController
|
|
486
|
+
while let presented = topController.presentedViewController {
|
|
487
|
+
topController = presented
|
|
488
|
+
}
|
|
489
|
+
topController.present(editorVC, animated: true)
|
|
490
|
+
|
|
491
|
+
} else {
|
|
492
|
+
guard let url = URL(string: uri) ?? URL(string: "file://" + uri.replacingOccurrences(of: "file://", with: "")) else {
|
|
493
|
+
reject("ERROR", "Invalid URI format", nil)
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
DispatchQueue.global().async {
|
|
498
|
+
if let imgData = try? Data(contentsOf: url), let image = UIImage(data: imgData) {
|
|
499
|
+
DispatchQueue.main.async {
|
|
500
|
+
let editorVC = ImageEditorViewController()
|
|
501
|
+
editorVC.languageCode = options["languageCode"] as? String ?? "en"
|
|
502
|
+
editorVC.standaloneImage = image
|
|
503
|
+
editorVC.currentIndex = 0
|
|
504
|
+
editorVC.modalPresentationStyle = .fullScreen
|
|
505
|
+
editorVC.singlePhotoMode = true
|
|
506
|
+
if let color = themeColor {
|
|
507
|
+
editorVC.themeColor = color
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
editorVC.onConfirm = { [weak self, weak editorVC] (editedAsset, editedImage, filterState, caption) in
|
|
511
|
+
editorVC?.dismiss(animated: true) {
|
|
512
|
+
self?.processEditorResult(asset: editedAsset, croppedImage: editedImage, filterState: filterState, caption: caption, galleryVC: nil, targetMaxWidth: parsedMaxWidth, targetMaxHeight: parsedMaxHeight, resolve: { resultArr in
|
|
513
|
+
var resultDict = (resultArr as? [[String: Any]])?.first ?? (resultArr as? [String: Any]) ?? [:]
|
|
514
|
+
// For remote URLs, we preserve originalUri if editedFileUri isn't provided
|
|
515
|
+
let originalRemoteUrl = (options["editedFileUri"] as? String) ?? uri
|
|
516
|
+
resultDict["originalUri"] = originalRemoteUrl
|
|
517
|
+
resolve(resultDict)
|
|
518
|
+
}, reject: reject)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
editorVC.onCancel = { [weak editorVC] in
|
|
523
|
+
editorVC?.dismiss(animated: true) {
|
|
524
|
+
reject("CANCELLED", "User cancelled editor", nil)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
var topController = rootViewController
|
|
529
|
+
while let presented = topController.presentedViewController {
|
|
530
|
+
topController = presented
|
|
531
|
+
}
|
|
532
|
+
topController.present(editorVC, animated: true)
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
DispatchQueue.main.async {
|
|
536
|
+
reject("ERROR", "Failed to load image from URI", nil)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
@objc
|
|
546
|
+
func openGallery(_ options: NSDictionary,
|
|
547
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
548
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
549
|
+
let startTime = CFAbsoluteTimeGetCurrent()
|
|
550
|
+
print("✅ [DEBUG] received options: \(options)")
|
|
551
|
+
print("🔵 [DEBUG] openGallery 시작 - 시간: \(startTime)")
|
|
552
|
+
|
|
553
|
+
// Main thread에서 실행
|
|
554
|
+
DispatchQueue.main.async { [weak self] () -> Void in
|
|
555
|
+
guard let self = self else {
|
|
556
|
+
reject("ERROR", "Module deallocated", nil)
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
print("🔵 [DEBUG] workItem 실행 시작 - 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - startTime))초")
|
|
561
|
+
|
|
562
|
+
let step1 = CFAbsoluteTimeGetCurrent()
|
|
563
|
+
guard let rootViewController = self.getRootViewController() else {
|
|
564
|
+
print("❌ [DEBUG] getRootViewController 실패")
|
|
565
|
+
reject("ERROR", "Unable to get root view controller", nil)
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
print("🔵 [DEBUG] getRootViewController 완료 - 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - step1))초")
|
|
569
|
+
|
|
570
|
+
let step2 = CFAbsoluteTimeGetCurrent()
|
|
571
|
+
print("🔵 [DEBUG] GalleryViewController 생성 시작...")
|
|
572
|
+
let galleryVC = GalleryViewController()
|
|
573
|
+
print("🔵 [DEBUG] GalleryViewController 생성 완료 - 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - step2))초")
|
|
574
|
+
|
|
575
|
+
// 현재 갤러리 참조 저장
|
|
576
|
+
self.currentGalleryVC = galleryVC
|
|
577
|
+
|
|
578
|
+
// 선택 변경 이벤트 콜백 설정
|
|
579
|
+
galleryVC.onSelectionChanged = { [weak self] selectedCount, maxSelection in
|
|
580
|
+
self?.sendEvent(withName: "onSelectionChange", body: [
|
|
581
|
+
"selectedCount": selectedCount,
|
|
582
|
+
"maxSelection": maxSelection
|
|
583
|
+
])
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// 선택 방식 (selectionMode: 'single' | 'multi', 생략 시 기존 규칙)
|
|
587
|
+
// - 'multi' : maxSelection 이 1 이어도 멀티와 똑같이 (autoCloseOnSelect 무시, 1 미만이면 1장)
|
|
588
|
+
// - 'single': 기존 단일 동작 (maxSelection 을 1로, autoCloseOnSelect 면 0)
|
|
589
|
+
// - 생략 : 기존 그대로 (autoCloseOnSelect → 0, 아니면 maxSelection)
|
|
590
|
+
let selectionMode = (options["selectionMode"] as? String)?.lowercased()
|
|
591
|
+
let autoCloseOnSelect = options["autoCloseOnSelect"] as? Bool ?? false
|
|
592
|
+
galleryVC.forceMultiMode = (selectionMode == "multi")
|
|
593
|
+
if selectionMode == "multi" {
|
|
594
|
+
let requested = options["maxSelection"] as? Int ?? 1
|
|
595
|
+
galleryVC.maxSelection = requested < 1 ? 1 : requested
|
|
596
|
+
} else if autoCloseOnSelect {
|
|
597
|
+
// maxSelection 설정 (autoCloseOnSelect가 true면 0으로 설정하여 단일 선택 자동 닫기 처리)
|
|
598
|
+
galleryVC.maxSelection = 0
|
|
599
|
+
} else if selectionMode == "single" {
|
|
600
|
+
galleryVC.maxSelection = 1
|
|
601
|
+
} else if let maxSelection = options["maxSelection"] as? Int {
|
|
602
|
+
galleryVC.maxSelection = maxSelection
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// maxWidth, maxHeight 옵션 (미설정 또는 0이면 원본 크기 유지)
|
|
606
|
+
galleryVC.maxWidth = (options["maxWidth"] as? Int) ?? 0
|
|
607
|
+
galleryVC.maxHeight = (options["maxHeight"] as? Int) ?? 0
|
|
608
|
+
|
|
609
|
+
// outputFormat: "webp" | "jpg" | "png" (기본값: "webp")
|
|
610
|
+
let fmt = (options["outputFormat"] as? String)?.lowercased() ?? "webp"
|
|
611
|
+
galleryVC.outputFormat = ["webp", "jpg", "png"].contains(fmt) ? fmt : "webp"
|
|
612
|
+
|
|
613
|
+
if let themeColorHex = options["themeColor"] as? String {
|
|
614
|
+
galleryVC.themeColorHex = themeColorHex
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 언어 코드 설정 (기본값: "en") - 이것이 먼저 설정되어 텍스트 자동 적용
|
|
618
|
+
galleryVC.languageCode = options["languageCode"] as? String ?? "en"
|
|
619
|
+
|
|
620
|
+
// 개별 텍스트 커스터마이징 (옵션, languageCode 오버라이드)
|
|
621
|
+
if let allText = options["allItemsText"] as? String {
|
|
622
|
+
galleryVC.allItemsText = allText
|
|
623
|
+
}
|
|
624
|
+
if let selectedText = options["selectedItemsText"] as? String {
|
|
625
|
+
galleryVC.selectedItemsText = selectedText
|
|
626
|
+
}
|
|
627
|
+
if let doneText = options["doneButtonText"] as? String {
|
|
628
|
+
galleryVC.doneButtonText = doneText
|
|
629
|
+
}
|
|
630
|
+
if let recentsText = options["recentsAlbumText"] as? String {
|
|
631
|
+
galleryVC.recentsAlbumText = recentsText
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
let enableEditor = options["enableEditor"] as? Bool ?? false
|
|
635
|
+
galleryVC.profileMode = options["profileMode"] as? Bool ?? false
|
|
636
|
+
|
|
637
|
+
if galleryVC.profileMode {
|
|
638
|
+
galleryVC.allowsEditing = enableEditor
|
|
639
|
+
galleryVC.onProfileCropComplete = { [weak self, weak galleryVC] asset, croppedImage in
|
|
640
|
+
guard let self = self, let currentGallery = galleryVC else { return }
|
|
641
|
+
|
|
642
|
+
if enableEditor {
|
|
643
|
+
let editorVC = ImageEditorViewController()
|
|
644
|
+
editorVC.languageCode = options["languageCode"] as? String ?? "en"
|
|
645
|
+
editorVC.allAssets = [asset]
|
|
646
|
+
editorVC.currentIndex = 0
|
|
647
|
+
editorVC.singlePhotoMode = true
|
|
648
|
+
editorVC.disableCrop = true
|
|
649
|
+
editorVC.croppedImages = [0: croppedImage]
|
|
650
|
+
editorVC.modalPresentationStyle = .overFullScreen
|
|
651
|
+
|
|
652
|
+
if let hex = currentGallery.themeColorHex, let color = UIColor(hexString: hex) {
|
|
653
|
+
editorVC.themeColor = color
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
editorVC.onConfirm = { [weak self] (editedAsset: PHAsset?, finalCroppedImage: UIImage?, filterState: FilterState, caption: String) in
|
|
657
|
+
guard let self = self else { return }
|
|
658
|
+
let navToDismiss = currentGallery.navigationController ?? currentGallery
|
|
659
|
+
let presenter = navToDismiss.presentingViewController ?? navToDismiss
|
|
660
|
+
presenter.dismiss(animated: true) {
|
|
661
|
+
self.processEditorResult(asset: editedAsset, croppedImage: finalCroppedImage ?? croppedImage, filterState: filterState, caption: caption, galleryVC: currentGallery, resolve: resolve, reject: reject)
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
editorVC.onCancel = { [weak currentGallery] in
|
|
666
|
+
currentGallery?.selectedAssets.removeAll()
|
|
667
|
+
currentGallery?.refreshSelectionAfterEdit()
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
var savedDetent: UISheetPresentationController.Detent.Identifier? = nil
|
|
671
|
+
if #available(iOS 15.0, *) {
|
|
672
|
+
savedDetent = currentGallery.navigationController?.sheetPresentationController?.selectedDetentIdentifier
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
let presenterVC = currentGallery.presentedViewController ?? currentGallery
|
|
676
|
+
presenterVC.present(editorVC, animated: true) {
|
|
677
|
+
if #available(iOS 15.0, *) {
|
|
678
|
+
if let saved = savedDetent {
|
|
679
|
+
currentGallery.navigationController?.sheetPresentationController?.selectedDetentIdentifier = saved
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
} else {
|
|
684
|
+
// Edit is disabled, directly process the cropped image
|
|
685
|
+
let navToDismiss = currentGallery.navigationController ?? currentGallery
|
|
686
|
+
let presenter = navToDismiss.presentingViewController ?? navToDismiss
|
|
687
|
+
presenter.dismiss(animated: true) {
|
|
688
|
+
self.processEditorResult(asset: asset, croppedImage: croppedImage, filterState: FilterState(filterId: "original", intensity: 1.0), caption: "", galleryVC: currentGallery, resolve: resolve, reject: reject)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if enableEditor {
|
|
695
|
+
// allowsEditing 활성화: GalleryVC 내 탭 동작 변경
|
|
696
|
+
galleryVC.allowsEditing = true
|
|
697
|
+
|
|
698
|
+
galleryVC.onSingleImageTappedForEdit = { [weak self] (asset: PHAsset, sourceFrame: CGRect, sourceImage: UIImage?) -> Void in
|
|
699
|
+
self?._turbo_handleSingleImageTappedForEdit(asset: asset, sourceFrame: sourceFrame, sourceImage: sourceImage, options: options, resolve: resolve, reject: reject)
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
galleryVC.onImagesSelected = { [weak self] selectedImages in
|
|
704
|
+
guard let self = self else { resolve([]); return }
|
|
705
|
+
|
|
706
|
+
guard !selectedImages.isEmpty else {
|
|
707
|
+
self.currentGalleryVC = nil
|
|
708
|
+
reject("CANCELLED", "User cancelled the image picker", nil)
|
|
709
|
+
return
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
var results: [[String: Any]] = []
|
|
713
|
+
let tempDir = self.prepareCacheDirectory()
|
|
714
|
+
let maxWidth = galleryVC.maxWidth
|
|
715
|
+
let maxHeight = galleryVC.maxHeight
|
|
716
|
+
let format = galleryVC.outputFormat // "webp" | "jpg" | "png"
|
|
717
|
+
let mimeType = self.mimeType(for: format)
|
|
718
|
+
let needResize = (maxWidth > 0 || maxHeight > 0)
|
|
719
|
+
|
|
720
|
+
let resultsLock = NSLock()
|
|
721
|
+
|
|
722
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
723
|
+
for (index, pair) in selectedImages.enumerated() {
|
|
724
|
+
let (asset, image) = pair
|
|
725
|
+
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
726
|
+
let originalSize = image.size
|
|
727
|
+
|
|
728
|
+
if let phAsset = asset {
|
|
729
|
+
// ✅ 1순위: PHAsset
|
|
730
|
+
let originalUri = "ph://\(phAsset.localIdentifier)"
|
|
731
|
+
|
|
732
|
+
var targetImage: UIImage
|
|
733
|
+
if needResize, let resized = self.resizeImage(image, maxWidth: maxWidth, maxHeight: maxHeight) {
|
|
734
|
+
targetImage = resized
|
|
735
|
+
} else {
|
|
736
|
+
targetImage = image
|
|
737
|
+
}
|
|
738
|
+
targetImage = ImageProcessor.shared.applyWatermarkIfNeeded(targetImage)
|
|
739
|
+
|
|
740
|
+
var result: [String: Any] = [
|
|
741
|
+
"originalUri": originalUri,
|
|
742
|
+
"originalWidth": Int(originalSize.width),
|
|
743
|
+
"originalHeight": Int(originalSize.height),
|
|
744
|
+
"type": mimeType
|
|
745
|
+
]
|
|
746
|
+
|
|
747
|
+
if let savedUri = self.saveImage(targetImage, format: format,
|
|
748
|
+
prefix: "resized",
|
|
749
|
+
index: index, timestamp: timestamp,
|
|
750
|
+
tempDir: tempDir) {
|
|
751
|
+
result["uri"] = savedUri
|
|
752
|
+
result["width"] = Int(targetImage.size.width)
|
|
753
|
+
result["height"] = Int(targetImage.size.height)
|
|
754
|
+
let meta = self.fileMetadata(fromPath: savedUri)
|
|
755
|
+
result["fileName"] = meta.fileName
|
|
756
|
+
result["fileExtension"] = meta.fileExtension
|
|
757
|
+
result["fileSize"] = meta.fileSize
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
resultsLock.lock()
|
|
761
|
+
results.append(result)
|
|
762
|
+
let currentResult = result
|
|
763
|
+
resultsLock.unlock()
|
|
764
|
+
|
|
765
|
+
self.sendEvent(withName: "onImageProcessed", body: [
|
|
766
|
+
"index": index,
|
|
767
|
+
"total": selectedImages.count,
|
|
768
|
+
"image": currentResult
|
|
769
|
+
])
|
|
770
|
+
} else {
|
|
771
|
+
// ✅ 2순위: 카메라 촬영본
|
|
772
|
+
guard let originalFilePath = self.saveOriginalImageAsJPEG(
|
|
773
|
+
image, index: index, timestamp: timestamp, tempDir: tempDir) else {
|
|
774
|
+
return
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
var targetImage: UIImage
|
|
778
|
+
if needResize, let resized = self.resizeImage(image, maxWidth: maxWidth, maxHeight: maxHeight) {
|
|
779
|
+
targetImage = resized
|
|
780
|
+
} else {
|
|
781
|
+
targetImage = image
|
|
782
|
+
}
|
|
783
|
+
targetImage = ImageProcessor.shared.applyWatermarkIfNeeded(targetImage)
|
|
784
|
+
|
|
785
|
+
var result: [String: Any] = [
|
|
786
|
+
"originalUri": originalFilePath,
|
|
787
|
+
"originalFileUri": originalFilePath,
|
|
788
|
+
"originalWidth": Int(originalSize.width),
|
|
789
|
+
"originalHeight": Int(originalSize.height),
|
|
790
|
+
"type": mimeType
|
|
791
|
+
]
|
|
792
|
+
let origMeta = self.fileMetadata(fromPath: originalFilePath)
|
|
793
|
+
result["fileName"] = origMeta.fileName
|
|
794
|
+
result["fileExtension"] = origMeta.fileExtension
|
|
795
|
+
result["fileSize"] = origMeta.fileSize
|
|
796
|
+
|
|
797
|
+
if let savedUri = self.saveImage(targetImage, format: format,
|
|
798
|
+
prefix: "resized",
|
|
799
|
+
index: index, timestamp: timestamp,
|
|
800
|
+
tempDir: tempDir) {
|
|
801
|
+
result["uri"] = savedUri
|
|
802
|
+
result["width"] = Int(targetImage.size.width)
|
|
803
|
+
result["height"] = Int(targetImage.size.height)
|
|
804
|
+
let meta = self.fileMetadata(fromPath: savedUri)
|
|
805
|
+
result["fileName"] = meta.fileName
|
|
806
|
+
result["fileExtension"] = meta.fileExtension
|
|
807
|
+
result["fileSize"] = meta.fileSize
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
resultsLock.lock()
|
|
811
|
+
results.append(result)
|
|
812
|
+
let currentResult = result
|
|
813
|
+
resultsLock.unlock()
|
|
814
|
+
|
|
815
|
+
self.sendEvent(withName: "onImageProcessed", body: [
|
|
816
|
+
"index": index,
|
|
817
|
+
"total": selectedImages.count,
|
|
818
|
+
"image": currentResult
|
|
819
|
+
])
|
|
820
|
+
}
|
|
821
|
+
} // end for
|
|
822
|
+
|
|
823
|
+
DispatchQueue.main.async {
|
|
824
|
+
self.currentGalleryVC = nil
|
|
825
|
+
resolve(results)
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
let navigationController = UINavigationController(rootViewController: galleryVC)
|
|
831
|
+
navigationController.modalPresentationStyle = .custom
|
|
832
|
+
navigationController.transitioningDelegate = galleryVC.customTransitioningDelegate
|
|
833
|
+
|
|
834
|
+
// navigationController.view.backgroundColor 설정 제거 (투명한 탑바 지원을 위함)
|
|
835
|
+
|
|
836
|
+
let step3 = CFAbsoluteTimeGetCurrent()
|
|
837
|
+
// Configure sheet presentation - 최소 설정만 사용 (기본 iOS 동작 보장)
|
|
838
|
+
if #available(iOS 15.0, *) {
|
|
839
|
+
if let sheet = navigationController.sheetPresentationController {
|
|
840
|
+
if #available(iOS 16.0, *) {
|
|
841
|
+
// iOS 16+: Use custom 60% detent
|
|
842
|
+
let customDetent = UISheetPresentationController.Detent.custom { context in
|
|
843
|
+
return context.maximumDetentValue * 0.6
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
sheet.detents = [customDetent, .large()]
|
|
847
|
+
} else {
|
|
848
|
+
// iOS 15: Use medium and large detents
|
|
849
|
+
sheet.detents = [.medium(), .large()]
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
sheet.prefersGrabberVisible = false
|
|
853
|
+
sheet.prefersScrollingExpandsWhenScrolledToEdge = true
|
|
854
|
+
|
|
855
|
+
if #available(iOS 16.0, *) {
|
|
856
|
+
sheet.preferredCornerRadius = 20
|
|
857
|
+
}
|
|
858
|
+
sheet.largestUndimmedDetentIdentifier = nil
|
|
859
|
+
|
|
860
|
+
if #available(iOS 16.0, *) {
|
|
861
|
+
sheet.preferredCornerRadius = 20
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
print("🔵 [DEBUG] Sheet 설정 완료 - 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - step3))초")
|
|
866
|
+
|
|
867
|
+
let step4 = CFAbsoluteTimeGetCurrent()
|
|
868
|
+
print("🔵 [DEBUG] present 시작...")
|
|
869
|
+
rootViewController.present(navigationController, animated: true)
|
|
870
|
+
print("🔵 [DEBUG] present 완료 - 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - step4))초")
|
|
871
|
+
print("✅ [DEBUG] openGallery 전체 완료 - 총 경과: \(String(format: "%.3f", CFAbsoluteTimeGetCurrent() - startTime))초")
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
@objc
|
|
876
|
+
func closeGallery(_ resolve: @escaping RCTPromiseResolveBlock,
|
|
877
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
878
|
+
|
|
879
|
+
DispatchQueue.main.async { [weak self] () -> Void in
|
|
880
|
+
guard let self = self else {
|
|
881
|
+
reject("ERROR", "Module deallocated", nil)
|
|
882
|
+
return
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
guard let galleryVC = self.currentGalleryVC else {
|
|
886
|
+
reject("ERROR", "Gallery is not open", nil)
|
|
887
|
+
return
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// 갤러리 닫기 전 강제 정리
|
|
891
|
+
galleryVC.forceCleanupBeforeDismiss()
|
|
892
|
+
|
|
893
|
+
// 갤러리 닫기
|
|
894
|
+
galleryVC.dismiss(animated: true) { [weak self] in
|
|
895
|
+
// dismiss 완료 후 참조 정리
|
|
896
|
+
self?.currentGalleryVC = nil
|
|
897
|
+
resolve(true)
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// MARK: - Private Methods
|
|
903
|
+
|
|
904
|
+
/// iOS가 저장 공간 부족 시 자동으로 정리하는 캐시 디렉토리
|
|
905
|
+
/// picker 결과물은 일시적으로만 필요하므로 cache 사용
|
|
906
|
+
private var cacheDirectory: String {
|
|
907
|
+
return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
|
|
908
|
+
.first?.appendingPathComponent("RNAttachmentKit").path
|
|
909
|
+
?? NSTemporaryDirectory()
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/// 캐시 디렉토리 준비 (없으면 생성)
|
|
913
|
+
private func prepareCacheDirectory() -> String {
|
|
914
|
+
let dir = cacheDirectory
|
|
915
|
+
if !FileManager.default.fileExists(atPath: dir) {
|
|
916
|
+
try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
|
|
917
|
+
}
|
|
918
|
+
return dir
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// 이미지 리사이징 (비율 유지)
|
|
922
|
+
private func resizeImage(_ image: UIImage, maxWidth: Int, maxHeight: Int) -> UIImage? {
|
|
923
|
+
let originalSize = image.size
|
|
924
|
+
var targetSize = originalSize
|
|
925
|
+
|
|
926
|
+
// maxWidth와 maxHeight 둘 다 사용 (기본값 500이므로 항상 리사이징)
|
|
927
|
+
if maxWidth > 0 && maxHeight > 0 {
|
|
928
|
+
// 둘 다 설정된 경우: 더 작은 비율로 리사이징
|
|
929
|
+
let widthRatio = CGFloat(maxWidth) / originalSize.width
|
|
930
|
+
let heightRatio = CGFloat(maxHeight) / originalSize.height
|
|
931
|
+
let ratio = min(widthRatio, heightRatio, 1.0) // 1.0 이상이면 리사이징 안 함
|
|
932
|
+
|
|
933
|
+
if ratio < 1.0 {
|
|
934
|
+
targetSize = CGSize(width: originalSize.width * ratio, height: originalSize.height * ratio)
|
|
935
|
+
}
|
|
936
|
+
} else if maxWidth > 0 && originalSize.width > CGFloat(maxWidth) {
|
|
937
|
+
// maxWidth만 설정된 경우
|
|
938
|
+
let ratio = CGFloat(maxWidth) / originalSize.width
|
|
939
|
+
targetSize = CGSize(width: CGFloat(maxWidth), height: originalSize.height * ratio)
|
|
940
|
+
} else if maxHeight > 0 && originalSize.height > CGFloat(maxHeight) {
|
|
941
|
+
// maxHeight만 설정된 경우
|
|
942
|
+
let ratio = CGFloat(maxHeight) / originalSize.height
|
|
943
|
+
targetSize = CGSize(width: originalSize.width * ratio, height: CGFloat(maxHeight))
|
|
944
|
+
} else {
|
|
945
|
+
// 리사이징 불필요
|
|
946
|
+
return nil
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// 같은 크기면 리사이징 안 함
|
|
950
|
+
if targetSize.width >= originalSize.width && targetSize.height >= originalSize.height {
|
|
951
|
+
return nil
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// 이미지 리사이징 (UIGraphicsImageRenderer 방식 - 메모리 최적화)
|
|
955
|
+
let format = UIGraphicsImageRendererFormat()
|
|
956
|
+
format.scale = 1.0 // 실제 픽셀 크기로 저장하기 위함
|
|
957
|
+
format.opaque = false
|
|
958
|
+
if #available(iOS 12.0, *) {
|
|
959
|
+
format.preferredRange = .standard // 광색역 대신 표준 색역 사용으로 메모리 절약
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
let renderer = UIGraphicsImageRenderer(size: targetSize, format: format)
|
|
963
|
+
return renderer.image { _ in
|
|
964
|
+
image.draw(in: CGRect(origin: .zero, size: targetSize))
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// ✅ 2순위: 원본 이미지를 JPEG으로 저장 (WebP 인코딩 없음 → 빠름)
|
|
969
|
+
// 카메라 촬영본 및 에디터 결과물에 사용
|
|
970
|
+
private func saveOriginalImageAsJPEG(_ image: UIImage, index: Int, timestamp: Int, tempDir: String) -> String? {
|
|
971
|
+
let fileName = "turbo_picker_original_\(timestamp)_\(index).jpg"
|
|
972
|
+
let filePath = (tempDir as NSString).appendingPathComponent(fileName)
|
|
973
|
+
let fileURL = URL(fileURLWithPath: filePath)
|
|
974
|
+
|
|
975
|
+
if let jpegData = image.jpegData(compressionQuality: 0.9) {
|
|
976
|
+
do {
|
|
977
|
+
try jpegData.write(to: fileURL, options: [.atomic])
|
|
978
|
+
return fileURL.absoluteString
|
|
979
|
+
} catch {
|
|
980
|
+
print("❌ [TurboImagePicker] JPEG 원본 이미지 저장 실패: \(error)")
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return nil
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// 리사이즈된 이미지 저장 (WebP 포맷)
|
|
987
|
+
private func saveResizedImage(_ image: UIImage, index: Int, timestamp: Int, tempDir: String) -> String? {
|
|
988
|
+
let fileName = "turbo_picker_resized_\(timestamp)_\(index).webp"
|
|
989
|
+
let filePath = (tempDir as NSString).appendingPathComponent(fileName)
|
|
990
|
+
let fileURL = URL(fileURLWithPath: filePath)
|
|
991
|
+
|
|
992
|
+
if let data = encodeWebP(image: image, quality: 0.85) {
|
|
993
|
+
do {
|
|
994
|
+
try data.write(to: fileURL, options: [.atomic])
|
|
995
|
+
return fileURL.absoluteString
|
|
996
|
+
} catch {
|
|
997
|
+
print("❌ [TurboImagePicker] WebP 리사이즈 이미지 저장 실패: \(error)")
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// Fallback to JPEG
|
|
1001
|
+
if let jpegData = image.jpegData(compressionQuality: 0.85) {
|
|
1002
|
+
let fallbackPath = (tempDir as NSString).appendingPathComponent("turbo_picker_resized_\(timestamp)_\(index).jpg")
|
|
1003
|
+
let fallbackURL = URL(fileURLWithPath: fallbackPath)
|
|
1004
|
+
do {
|
|
1005
|
+
try jpegData.write(to: fallbackURL, options: [.atomic])
|
|
1006
|
+
return fallbackURL.absoluteString
|
|
1007
|
+
} catch {}
|
|
1008
|
+
}
|
|
1009
|
+
return nil
|
|
1010
|
+
}
|
|
1011
|
+
// WebP 인코딩 - iOS 16+ 네이티브 CGImageDestination 사용 또는 런타임 Fallback
|
|
1012
|
+
private func encodeWebP(image: UIImage, quality: CGFloat) -> Data? {
|
|
1013
|
+
if #available(iOS 16.0, *) {
|
|
1014
|
+
if let cgImage = image.cgImage {
|
|
1015
|
+
let data = NSMutableData()
|
|
1016
|
+
if let destination = CGImageDestinationCreateWithData(data as CFMutableData, "public.webp" as CFString, 1, nil) {
|
|
1017
|
+
let options: [CFString: Any] = [kCGImageDestinationLossyCompressionQuality: quality]
|
|
1018
|
+
CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
|
|
1019
|
+
if CGImageDestinationFinalize(destination) {
|
|
1020
|
+
NSLog("✅ [encodeWebP] iOS 16 네이티브 WebP 인코딩 성공 - %d bytes", data.length)
|
|
1021
|
+
return data as Data
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
NSLog("🟣 [encodeWebP] SDImageWebPCoder 런타임 폴백 시도")
|
|
1028
|
+
// iOS 14, 15이거나 네이티브 실패 시 SDImageWebPCoder 런타임 호출
|
|
1029
|
+
if let coderClass = NSClassFromString("SDImageWebPCoder") as? NSObject.Type {
|
|
1030
|
+
if coderClass.responds(to: NSSelectorFromString("sharedCoder")) {
|
|
1031
|
+
let sharedCoder = coderClass.perform(NSSelectorFromString("sharedCoder")).takeUnretainedValue()
|
|
1032
|
+
let encodeSel = NSSelectorFromString("encodedDataWithImage:format:options:")
|
|
1033
|
+
|
|
1034
|
+
if sharedCoder.responds(to: encodeSel),
|
|
1035
|
+
let method = class_getInstanceMethod(type(of: sharedCoder), encodeSel) {
|
|
1036
|
+
|
|
1037
|
+
// ObjC method signature: - (NSData *)encodedDataWithImage:(UIImage *)image format:(NSInteger)format options:(NSDictionary *)options
|
|
1038
|
+
typealias EncodeFunc = @convention(c) (AnyObject, Selector, UIImage?, Int, [String: Any]?) -> Data?
|
|
1039
|
+
let implementation = method_getImplementation(method)
|
|
1040
|
+
let encode = unsafeBitCast(implementation, to: EncodeFunc.self)
|
|
1041
|
+
|
|
1042
|
+
// format: 3 (SDImageFormatWebP)
|
|
1043
|
+
let options: [String: Any] = ["encodeCompressionQuality": quality]
|
|
1044
|
+
if let data = encode(sharedCoder, encodeSel, image, 3, options) {
|
|
1045
|
+
NSLog("✅ [encodeWebP] SDImageWebPCoder 런타임 인코딩 성공 - %d bytes", data.count)
|
|
1046
|
+
return data
|
|
1047
|
+
} else {
|
|
1048
|
+
NSLog("❌ [encodeWebP] SDImageWebPCoder 런타임 인코딩 실패 (nil 반환)")
|
|
1049
|
+
}
|
|
1050
|
+
} else {
|
|
1051
|
+
NSLog("❌ [encodeWebP] SDImageWebPCoder에 encodedDataWithImage:format:options: 메서드가 없습니다.")
|
|
1052
|
+
}
|
|
1053
|
+
} else {
|
|
1054
|
+
NSLog("❌ [encodeWebP] SDImageWebPCoder에 sharedCoder가 없습니다.")
|
|
1055
|
+
}
|
|
1056
|
+
} else {
|
|
1057
|
+
NSLog("❌ [encodeWebP] SDImageWebPCoder 클래스를 찾을 수 없습니다.")
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
NSLog("❌ [encodeWebP] 모든 WebP 인코딩 실패 → nil 반환")
|
|
1061
|
+
return nil
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
// MARK: - Format-aware save helpers
|
|
1066
|
+
|
|
1067
|
+
/// outputFormat("webp"|"jpg"|"png")에 맞는 MIME 타입 반환
|
|
1068
|
+
private func mimeType(for format: String) -> String {
|
|
1069
|
+
switch format {
|
|
1070
|
+
case "png": return "image/png"
|
|
1071
|
+
case "jpg": return "image/jpeg"
|
|
1072
|
+
default: return "image/webp"
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/// 이미지를 지정 포맷으로 저장하고 file:// URI 반환
|
|
1077
|
+
/// - Parameters:
|
|
1078
|
+
/// - image: 저장할 UIImage
|
|
1079
|
+
/// - format: "webp" | "jpg" | "png"
|
|
1080
|
+
/// - prefix: 파일명 prefix (e.g. "resized", "original")
|
|
1081
|
+
/// - index: 배치 내 인덱스
|
|
1082
|
+
/// - timestamp: 타임스탬프(ms)
|
|
1083
|
+
/// - tempDir: 저장 디렉토리 경로
|
|
1084
|
+
private func saveImage(_ image: UIImage, format: String,
|
|
1085
|
+
prefix: String, index: Int,
|
|
1086
|
+
timestamp: Int, tempDir: String, forceJPEG: Bool = false) -> String? {
|
|
1087
|
+
let ext: String
|
|
1088
|
+
let data: Data?
|
|
1089
|
+
|
|
1090
|
+
let actualFormat = forceJPEG ? "jpg" : format
|
|
1091
|
+
|
|
1092
|
+
switch actualFormat {
|
|
1093
|
+
case "png":
|
|
1094
|
+
ext = "png"
|
|
1095
|
+
data = image.pngData()
|
|
1096
|
+
case "jpg":
|
|
1097
|
+
ext = "jpg"
|
|
1098
|
+
data = image.jpegData(compressionQuality: 0.9)
|
|
1099
|
+
default: // "webp"
|
|
1100
|
+
ext = "webp"
|
|
1101
|
+
data = encodeWebP(image: image, quality: 0.85)
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
guard let imageData = data else {
|
|
1105
|
+
// WebP 인코딩 실패 시 JPEG fallback
|
|
1106
|
+
if format == "webp", let fallbackData = image.jpegData(compressionQuality: 0.85) {
|
|
1107
|
+
let path = (tempDir as NSString).appendingPathComponent(
|
|
1108
|
+
"turbo_picker_\(prefix)_\(timestamp)_\(index).jpg")
|
|
1109
|
+
let url = URL(fileURLWithPath: path)
|
|
1110
|
+
try? fallbackData.write(to: url)
|
|
1111
|
+
return url.absoluteString
|
|
1112
|
+
}
|
|
1113
|
+
return nil
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
let fileName = "turbo_picker_\(prefix)_\(timestamp)_\(index).\(ext)"
|
|
1117
|
+
let filePath = (tempDir as NSString).appendingPathComponent(fileName)
|
|
1118
|
+
let fileURL = URL(fileURLWithPath: filePath)
|
|
1119
|
+
|
|
1120
|
+
do {
|
|
1121
|
+
try imageData.write(to: fileURL)
|
|
1122
|
+
return fileURL.absoluteString
|
|
1123
|
+
} catch {
|
|
1124
|
+
print("❌ [TurboImagePicker] \(format.uppercased()) 저장 실패: \(error)")
|
|
1125
|
+
return nil
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
/// 저장된 파일 경로에서 fileName, fileExtension, fileSize 반환
|
|
1131
|
+
private func fileMetadata(fromPath filePath: String) -> (fileName: String, fileExtension: String, fileSize: Int64) {
|
|
1132
|
+
let url = URL(fileURLWithPath: filePath.hasPrefix("file://")
|
|
1133
|
+
? String(filePath.dropFirst(7))
|
|
1134
|
+
: filePath)
|
|
1135
|
+
let fileName = url.lastPathComponent
|
|
1136
|
+
let fileExtension = url.pathExtension.lowercased()
|
|
1137
|
+
let fileSize = (try? FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int64) ?? 0
|
|
1138
|
+
return (fileName, fileExtension, fileSize)
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
private func getRootViewController() -> UIViewController? {
|
|
1142
|
+
var rootViewController: UIViewController?
|
|
1143
|
+
|
|
1144
|
+
if #available(iOS 13.0, *) {
|
|
1145
|
+
if let windowScene = UIApplication.shared.connectedScenes
|
|
1146
|
+
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene,
|
|
1147
|
+
let window = windowScene.windows.first(where: { $0.isKeyWindow }) {
|
|
1148
|
+
rootViewController = window.rootViewController
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if rootViewController == nil {
|
|
1153
|
+
rootViewController = UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if rootViewController == nil {
|
|
1157
|
+
rootViewController = UIApplication.shared.keyWindow?.rootViewController
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Handle presented view controllers
|
|
1161
|
+
while let presented = rootViewController?.presentedViewController {
|
|
1162
|
+
rootViewController = presented
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
return rootViewController
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// MARK: - Editor Result Processing
|
|
1169
|
+
|
|
1170
|
+
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) {
|
|
1171
|
+
let tempDir = prepareCacheDirectory()
|
|
1172
|
+
let maxWidth = targetMaxWidth ?? galleryVC?.maxWidth ?? 1024
|
|
1173
|
+
let maxHeight = targetMaxHeight ?? galleryVC?.maxHeight ?? 1024
|
|
1174
|
+
let format = galleryVC?.outputFormat ?? "webp"
|
|
1175
|
+
|
|
1176
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
1177
|
+
let photoManager = PhotoManager.shared
|
|
1178
|
+
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
1179
|
+
|
|
1180
|
+
let originalUri = asset != nil ? "ph://\(asset!.localIdentifier)" : ""
|
|
1181
|
+
var originalFilePath: String? = nil
|
|
1182
|
+
var resultImage: UIImage? = nil
|
|
1183
|
+
|
|
1184
|
+
let dispatchGroup = DispatchGroup()
|
|
1185
|
+
dispatchGroup.enter()
|
|
1186
|
+
|
|
1187
|
+
let handleBaseImage = { (baseImage: UIImage) in
|
|
1188
|
+
var finalImage = baseImage
|
|
1189
|
+
|
|
1190
|
+
if filterState.filterId != "original", let filter = FilterManager.shared.filters.first(where: { $0.id == filterState.filterId }) {
|
|
1191
|
+
if let ciImage = CIImage(image: baseImage),
|
|
1192
|
+
let out = filter.apply(ciImage, filterState.intensity),
|
|
1193
|
+
let cgImg = FilterManager.shared.context.createCGImage(out, from: out.extent) {
|
|
1194
|
+
finalImage = UIImage(cgImage: cgImg, scale: baseImage.scale, orientation: baseImage.imageOrientation)
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
finalImage = ImageProcessor.shared.applyWatermarkIfNeeded(finalImage)
|
|
1199
|
+
resultImage = finalImage
|
|
1200
|
+
// ✅ 2순위: 에디터 결과 원본 저장 (WebP 소프트웨어 인코딩 방지를 위해 하드웨어 가속되는 JPG 강제 사용)
|
|
1201
|
+
originalFilePath = self.saveImage(finalImage, format: format, prefix: "original", index: 0, timestamp: timestamp, tempDir: tempDir, forceJPEG: true)
|
|
1202
|
+
dispatchGroup.leave()
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
let targetSize = (maxWidth > 0 && maxHeight > 0) ? CGSize(width: maxWidth, height: maxHeight) : PHImageManagerMaximumSize
|
|
1206
|
+
|
|
1207
|
+
if let cropped = croppedImage {
|
|
1208
|
+
handleBaseImage(cropped)
|
|
1209
|
+
} else {
|
|
1210
|
+
_ = photoManager.requestFullImage(for: asset!, targetSize: targetSize, progressHandler: nil) { fullImage in
|
|
1211
|
+
if let fullImage = fullImage {
|
|
1212
|
+
handleBaseImage(fullImage)
|
|
1213
|
+
} else {
|
|
1214
|
+
dispatchGroup.leave()
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
dispatchGroup.wait()
|
|
1220
|
+
|
|
1221
|
+
guard let image = resultImage else {
|
|
1222
|
+
DispatchQueue.main.async {
|
|
1223
|
+
self.currentGalleryVC = nil
|
|
1224
|
+
reject("ERROR", "Failed to load image from editor", nil)
|
|
1225
|
+
}
|
|
1226
|
+
return
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
let originalSize = image.size
|
|
1230
|
+
|
|
1231
|
+
var result: [String: Any] = [
|
|
1232
|
+
"originalUri": originalUri,
|
|
1233
|
+
"originalWidth": Int(originalSize.width),
|
|
1234
|
+
"originalHeight": Int(originalSize.height),
|
|
1235
|
+
"type": self.mimeType(for: format),
|
|
1236
|
+
"caption": caption
|
|
1237
|
+
]
|
|
1238
|
+
|
|
1239
|
+
if let originalFilePath = originalFilePath {
|
|
1240
|
+
result["originalFileUri"] = originalFilePath
|
|
1241
|
+
result["uri"] = originalFilePath // 기본적으로 uri에도 파일 경로 저장
|
|
1242
|
+
let meta = self.fileMetadata(fromPath: originalFilePath)
|
|
1243
|
+
result["fileName"] = meta.fileName
|
|
1244
|
+
result["fileExtension"] = meta.fileExtension
|
|
1245
|
+
result["fileSize"] = meta.fileSize
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
let finalImageForFormat = self.resizeImage(image, maxWidth: maxWidth, maxHeight: maxHeight) ?? image
|
|
1249
|
+
|
|
1250
|
+
if let finalUri = self.saveImage(finalImageForFormat, format: format, prefix: "final", index: 0, timestamp: timestamp, tempDir: tempDir) {
|
|
1251
|
+
result["uri"] = finalUri
|
|
1252
|
+
result["width"] = Int(finalImageForFormat.size.width)
|
|
1253
|
+
result["height"] = Int(finalImageForFormat.size.height)
|
|
1254
|
+
let finalMeta = self.fileMetadata(fromPath: finalUri)
|
|
1255
|
+
result["fileName"] = finalMeta.fileName
|
|
1256
|
+
result["fileExtension"] = finalMeta.fileExtension
|
|
1257
|
+
result["fileSize"] = finalMeta.fileSize
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
DispatchQueue.main.async {
|
|
1261
|
+
self.currentGalleryVC = nil
|
|
1262
|
+
resolve([result])
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
private func processEditorResultMulti(assets: [PHAsset], croppedImages: [PHAsset: UIImage], filterStates: [PHAsset: FilterState], galleryVC: GalleryViewController, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
1269
|
+
let tempDir = prepareCacheDirectory()
|
|
1270
|
+
let maxWidth = galleryVC.maxWidth
|
|
1271
|
+
let maxHeight = galleryVC.maxHeight
|
|
1272
|
+
let format = galleryVC.outputFormat
|
|
1273
|
+
|
|
1274
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
1275
|
+
let photoManager = PhotoManager.shared
|
|
1276
|
+
var results: [[String: Any]] = []
|
|
1277
|
+
let dispatchGroup = DispatchGroup()
|
|
1278
|
+
let resultsLock = NSLock()
|
|
1279
|
+
|
|
1280
|
+
for (index, asset) in assets.enumerated() {
|
|
1281
|
+
dispatchGroup.enter()
|
|
1282
|
+
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
1283
|
+
let originalUri = "ph://\(asset.localIdentifier)"
|
|
1284
|
+
let filterState = filterStates[asset] ?? FilterState(filterId: "original", intensity: 1.0)
|
|
1285
|
+
let croppedImage = croppedImages[asset]
|
|
1286
|
+
|
|
1287
|
+
let handleBaseImage = { (baseImage: UIImage) in
|
|
1288
|
+
var finalImage = baseImage
|
|
1289
|
+
|
|
1290
|
+
if filterState.filterId != "original", let filter = FilterManager.shared.filters.first(where: { $0.id == filterState.filterId }) {
|
|
1291
|
+
if let ciImage = CIImage(image: baseImage),
|
|
1292
|
+
let out = filter.apply(ciImage, filterState.intensity),
|
|
1293
|
+
let cgImg = FilterManager.shared.context.createCGImage(out, from: out.extent) {
|
|
1294
|
+
finalImage = UIImage(cgImage: cgImg, scale: baseImage.scale, orientation: baseImage.imageOrientation)
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
finalImage = ImageProcessor.shared.applyWatermarkIfNeeded(finalImage)
|
|
1299
|
+
|
|
1300
|
+
var result: [String: Any] = [
|
|
1301
|
+
"originalUri": originalUri,
|
|
1302
|
+
"originalWidth": Int(finalImage.size.width),
|
|
1303
|
+
"originalHeight": Int(finalImage.size.height),
|
|
1304
|
+
"type": self.mimeType(for: format)
|
|
1305
|
+
]
|
|
1306
|
+
|
|
1307
|
+
// ✅ 2순위: 에디터 다중 결과 원본 저장 (JPG 강제 사용으로 속도 최적화)
|
|
1308
|
+
if let originalFilePath = self.saveImage(finalImage, format: format, prefix: "original", index: index, timestamp: timestamp, tempDir: tempDir, forceJPEG: true) {
|
|
1309
|
+
result["originalFileUri"] = originalFilePath
|
|
1310
|
+
result["uri"] = originalFilePath // 기본적으로 uri에도 파일 경로 저장
|
|
1311
|
+
let meta = self.fileMetadata(fromPath: originalFilePath)
|
|
1312
|
+
result["fileName"] = meta.fileName
|
|
1313
|
+
result["fileExtension"] = meta.fileExtension
|
|
1314
|
+
result["fileSize"] = meta.fileSize
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
if (maxWidth > 0 || maxHeight > 0), let resizedImage = self.resizeImage(finalImage, maxWidth: maxWidth, maxHeight: maxHeight) {
|
|
1318
|
+
if let resizedUri = self.saveImage(resizedImage, format: format, prefix: "resized", index: index, timestamp: timestamp, tempDir: tempDir) {
|
|
1319
|
+
result["uri"] = resizedUri
|
|
1320
|
+
result["width"] = Int(resizedImage.size.width)
|
|
1321
|
+
result["height"] = Int(resizedImage.size.height)
|
|
1322
|
+
let resizedMeta = self.fileMetadata(fromPath: resizedUri)
|
|
1323
|
+
result["fileName"] = resizedMeta.fileName
|
|
1324
|
+
result["fileExtension"] = resizedMeta.fileExtension
|
|
1325
|
+
result["fileSize"] = resizedMeta.fileSize
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
resultsLock.lock()
|
|
1330
|
+
results.append(result)
|
|
1331
|
+
let currentResult = result
|
|
1332
|
+
resultsLock.unlock()
|
|
1333
|
+
|
|
1334
|
+
self.sendEvent(withName: "onImageProcessed", body: [
|
|
1335
|
+
"index": index,
|
|
1336
|
+
"total": assets.count,
|
|
1337
|
+
"image": currentResult
|
|
1338
|
+
])
|
|
1339
|
+
dispatchGroup.leave()
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
let targetSize = (maxWidth > 0 && maxHeight > 0) ? CGSize(width: maxWidth, height: maxHeight) : PHImageManagerMaximumSize
|
|
1343
|
+
|
|
1344
|
+
if let cropped = croppedImage {
|
|
1345
|
+
handleBaseImage(cropped)
|
|
1346
|
+
} else {
|
|
1347
|
+
_ = photoManager.requestFullImage(for: asset, targetSize: targetSize, progressHandler: nil) { fullImage in
|
|
1348
|
+
if let fullImage = fullImage {
|
|
1349
|
+
handleBaseImage(fullImage)
|
|
1350
|
+
} else {
|
|
1351
|
+
dispatchGroup.leave()
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
dispatchGroup.wait()
|
|
1358
|
+
|
|
1359
|
+
DispatchQueue.main.async {
|
|
1360
|
+
self.currentGalleryVC = nil
|
|
1361
|
+
if results.isEmpty {
|
|
1362
|
+
reject("ERROR", "Failed to load images from editor", nil)
|
|
1363
|
+
} else {
|
|
1364
|
+
// 순서를 유지하기 위해
|
|
1365
|
+
resolve(results)
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// MARK: - UIColor Extension for Hex Support
|
|
1373
|
+
extension UIColor {
|
|
1374
|
+
convenience init?(hexString: String) {
|
|
1375
|
+
var cString: String = hexString.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
|
1376
|
+
if cString.hasPrefix("#") { cString.remove(at: cString.startIndex) }
|
|
1377
|
+
|
|
1378
|
+
var rgbValue: UInt64 = 0
|
|
1379
|
+
Scanner(string: cString).scanHexInt64(&rgbValue)
|
|
1380
|
+
|
|
1381
|
+
if cString.count == 6 {
|
|
1382
|
+
self.init(
|
|
1383
|
+
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
|
|
1384
|
+
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
|
|
1385
|
+
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
|
|
1386
|
+
alpha: 1.0
|
|
1387
|
+
)
|
|
1388
|
+
} else if cString.count == 8 {
|
|
1389
|
+
self.init(
|
|
1390
|
+
red: CGFloat((rgbValue & 0xFF000000) >> 24) / 255.0,
|
|
1391
|
+
green: CGFloat((rgbValue & 0x00FF0000) >> 16) / 255.0,
|
|
1392
|
+
blue: CGFloat((rgbValue & 0x0000FF00) >> 8) / 255.0,
|
|
1393
|
+
alpha: CGFloat(rgbValue & 0x000000FF) / 255.0
|
|
1394
|
+
)
|
|
1395
|
+
} else {
|
|
1396
|
+
return nil
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// MARK: - Media Gallery (사진 모아보기)
|
|
1402
|
+
|
|
1403
|
+
extension RNAttachmentKit {
|
|
1404
|
+
|
|
1405
|
+
/// openMediaGallery(options) → { galleryId }
|
|
1406
|
+
/// - dataSource: 등록된 네이티브 데이터 소스 이름 (MediaGalleryRegistry) — 페이지 요청이 JS 를 거치지 않음
|
|
1407
|
+
/// - initialItems / nextCursor / hasMore: JS 공급 방식 (끝에 가까우면 onGalleryNeedPage → appendGalleryItems)
|
|
1408
|
+
@objc
|
|
1409
|
+
func openMediaGallery(_ options: NSDictionary,
|
|
1410
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
1411
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
1412
|
+
let o = options as? [String: Any] ?? [:]
|
|
1413
|
+
DispatchQueue.main.async {
|
|
1414
|
+
let config = MediaGalleryConfig(dictionary: o)
|
|
1415
|
+
let galleryId = config.galleryId
|
|
1416
|
+
if self.mediaGalleries[galleryId] != nil {
|
|
1417
|
+
reject("E_GALLERY_OPEN", "Media gallery '\(galleryId)' is already open", nil)
|
|
1418
|
+
return
|
|
1419
|
+
}
|
|
1420
|
+
let store: MediaGalleryStore
|
|
1421
|
+
if let name = o["dataSource"] as? String, !name.isEmpty {
|
|
1422
|
+
guard let dataSource = MediaGalleryRegistry.source(named: name) else {
|
|
1423
|
+
reject("E_NO_DATA_SOURCE", "Media gallery data source '\(name)' is not registered", nil)
|
|
1424
|
+
return
|
|
1425
|
+
}
|
|
1426
|
+
let params = o["dataSourceParams"] as? [String: Any] ?? [:]
|
|
1427
|
+
store = MediaGalleryStore(galleryId: galleryId, source: .native(dataSource, params: params))
|
|
1428
|
+
} else {
|
|
1429
|
+
let raw = o["initialItems"] as? [[String: Any]] ?? []
|
|
1430
|
+
let items = raw.compactMap { MediaGalleryItem(dictionary: $0) }
|
|
1431
|
+
let hasMore = (o["hasMore"] as? NSNumber)?.boolValue ?? false
|
|
1432
|
+
store = MediaGalleryStore(galleryId: galleryId, source: .javascript, initialItems: items,
|
|
1433
|
+
nextCursor: o["nextCursor"] as? String, hasMore: hasMore)
|
|
1434
|
+
store.onNeedPage = { [weak self] cursor in
|
|
1435
|
+
let body: [String: Any] = ["galleryId": galleryId, "cursor": cursor.map { $0 as Any } ?? NSNull()]
|
|
1436
|
+
self?.sendEvent(withName: "onGalleryNeedPage", body: body)
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
guard let root = self.getRootViewController() else {
|
|
1440
|
+
reject("E_NO_ROOT", "No root view controller", nil)
|
|
1441
|
+
return
|
|
1442
|
+
}
|
|
1443
|
+
let vc = MediaGalleryViewController(store: store, config: config)
|
|
1444
|
+
vc.onClosed = { [weak self] in
|
|
1445
|
+
guard let self = self else { return }
|
|
1446
|
+
self.mediaGalleries.removeValue(forKey: galleryId)
|
|
1447
|
+
self.sendEvent(withName: "onGalleryClosed", body: ["galleryId": galleryId])
|
|
1448
|
+
}
|
|
1449
|
+
// 뷰어에서 삭제를 고르면: 모듈은 그 묶음을 그리드·뷰어에서 지우고, 실제 삭제는 앱이 하도록 이벤트만 보냄
|
|
1450
|
+
vc.onItemDeleted = { [weak self] item, option in
|
|
1451
|
+
var body: [String: Any] = ["galleryId": galleryId, "id": item.id, "groupId": item.groupId]
|
|
1452
|
+
body["option"] = option.map { $0 as Any } ?? NSNull()
|
|
1453
|
+
self?.sendEvent(withName: "onGalleryItemDeleted", body: body)
|
|
1454
|
+
}
|
|
1455
|
+
// 상단 바 메뉴에서 그리드 크기를 바꾸면 (모듈이 기기에 저장, 앱에도 알림)
|
|
1456
|
+
vc.onGridSizeChanged = { [weak self] columns in
|
|
1457
|
+
self?.sendEvent(withName: "onGalleryGridSizeChanged",
|
|
1458
|
+
body: ["galleryId": galleryId, "gridSize": MediaGalleryGridPreference.gridSizeString(columns)])
|
|
1459
|
+
}
|
|
1460
|
+
self.mediaGalleries[galleryId] = vc
|
|
1461
|
+
// 피커 갤러리와 같은 바텀시트로 표시
|
|
1462
|
+
root.present(vc.makePresentableController(), animated: true)
|
|
1463
|
+
resolve(["galleryId": galleryId])
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
@objc
|
|
1468
|
+
func closeMediaGallery(_ galleryId: String,
|
|
1469
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
1470
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
1471
|
+
DispatchQueue.main.async {
|
|
1472
|
+
guard let vc = self.mediaGalleries[galleryId] else {
|
|
1473
|
+
resolve(nil)
|
|
1474
|
+
return
|
|
1475
|
+
}
|
|
1476
|
+
// 갤러리 위에 띄운 화면(뷰어 등)까지 함께 닫음
|
|
1477
|
+
if let presenting = vc.navigationController?.presentingViewController ?? vc.presentingViewController {
|
|
1478
|
+
presenting.dismiss(animated: true) { resolve(nil) }
|
|
1479
|
+
} else {
|
|
1480
|
+
resolve(nil)
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/// JS 공급 방식: { galleryId, items, nextCursor?, hasMore }
|
|
1486
|
+
@objc
|
|
1487
|
+
func appendGalleryItems(_ payload: NSDictionary,
|
|
1488
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
1489
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
1490
|
+
let p = payload as? [String: Any] ?? [:]
|
|
1491
|
+
let galleryId = p["galleryId"] as? String ?? ""
|
|
1492
|
+
let raw = p["items"] as? [[String: Any]] ?? []
|
|
1493
|
+
let items = raw.compactMap { MediaGalleryItem(dictionary: $0) }
|
|
1494
|
+
let page = MediaGalleryPage(items: items, nextCursor: p["nextCursor"] as? String,
|
|
1495
|
+
hasMore: (p["hasMore"] as? NSNumber)?.boolValue ?? false)
|
|
1496
|
+
DispatchQueue.main.async {
|
|
1497
|
+
self.mediaGalleries[galleryId]?.store.appendExternal(page)
|
|
1498
|
+
resolve(nil)
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/// 앱이 다른 경로로 지운 묶음 반영: { galleryId, groupIds }
|
|
1503
|
+
@objc
|
|
1504
|
+
func removeGalleryItems(_ payload: NSDictionary,
|
|
1505
|
+
resolve: @escaping RCTPromiseResolveBlock,
|
|
1506
|
+
reject: @escaping RCTPromiseRejectBlock) -> Void {
|
|
1507
|
+
let p = payload as? [String: Any] ?? [:]
|
|
1508
|
+
let galleryId = p["galleryId"] as? String ?? ""
|
|
1509
|
+
let groupIds = (p["groupIds"] as? [Any] ?? []).compactMap { v -> String? in
|
|
1510
|
+
if let s = v as? String { return s }
|
|
1511
|
+
if let n = v as? NSNumber { return n.stringValue }
|
|
1512
|
+
return nil
|
|
1513
|
+
}
|
|
1514
|
+
DispatchQueue.main.async {
|
|
1515
|
+
self.mediaGalleries[galleryId]?.store.removeGroups(Set(groupIds))
|
|
1516
|
+
resolve(nil)
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
#if RCT_NEW_ARCH_ENABLED
|
|
1522
|
+
// MARK: - Turbo Module Protocol Conformance (New Architecture)
|
|
1523
|
+
// extension RNAttachmentKit: RCTTurboModule {
|
|
1524
|
+
// // Turbo Module specific implementation if needed
|
|
1525
|
+
// }
|
|
1526
|
+
#endif
|
|
1527
|
+
import Foundation
|
|
1528
|
+
import UIKit
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
fileprivate func _turbo_handleConfirmMulti(assets: [PHAsset], croppedImages: [PHAsset: UIImage], filterStates: [PHAsset: FilterState], currentGallery: GalleryViewController) {
|
|
1532
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
1533
|
+
var newlyEditedAssets: [PHAsset] = []
|
|
1534
|
+
for asset in assets {
|
|
1535
|
+
if let baseImage = croppedImages[asset] {
|
|
1536
|
+
var finalImage = baseImage
|
|
1537
|
+
if let state = filterStates[asset], state.filterId != "original" {
|
|
1538
|
+
if let filter = FilterManager.shared.filters.first(where: { (f: ImageFilter) -> Bool in return f.id == state.filterId }) {
|
|
1539
|
+
if let ciImage = CIImage(image: baseImage) {
|
|
1540
|
+
if let out = filter.apply(ciImage, state.intensity) {
|
|
1541
|
+
if let cgImg = FilterManager.shared.context.createCGImage(out, from: out.extent) {
|
|
1542
|
+
finalImage = UIImage(cgImage: cgImg, scale: baseImage.scale, orientation: baseImage.imageOrientation)
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
DispatchQueue.main.async {
|
|
1549
|
+
currentGallery.editedImages[asset.localIdentifier] = finalImage
|
|
1550
|
+
}
|
|
1551
|
+
newlyEditedAssets.append(asset)
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
DispatchQueue.main.async {
|
|
1555
|
+
for asset in newlyEditedAssets {
|
|
1556
|
+
if !currentGallery.selectedAssetsSet.contains(asset.localIdentifier) {
|
|
1557
|
+
if currentGallery.maxSelection == 0 || currentGallery.selectedAssets.count < currentGallery.maxSelection {
|
|
1558
|
+
currentGallery.selectedAssets.append(asset)
|
|
1559
|
+
currentGallery.selectedAssetsSet.insert(asset.localIdentifier)
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
currentGallery.refreshSelectionAfterEdit()
|
|
1564
|
+
|
|
1565
|
+
// These updates are required per the original code
|
|
1566
|
+
currentGallery.updateSelectedCellNumbers()
|
|
1567
|
+
currentGallery.notifySelectionChanged()
|
|
1568
|
+
currentGallery.updateNavigationBarForSelection()
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
fileprivate extension RNAttachmentKit {
|
|
1574
|
+
func _turbo_handleSingleImageTappedForEdit(asset: PHAsset, sourceFrame: CGRect, sourceImage: UIImage?, options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
1575
|
+
guard let currentGallery = self.currentGalleryVC else { return }
|
|
1576
|
+
let galleryNav = currentGallery.navigationController
|
|
1577
|
+
let isMultiSelect = currentGallery.forceMultiMode || (currentGallery.maxSelection != 1 && currentGallery.maxSelection != 0)
|
|
1578
|
+
|
|
1579
|
+
let editorVC = ImageEditorViewController()
|
|
1580
|
+
editorVC.languageCode = options["languageCode"] as? String ?? "en"
|
|
1581
|
+
var editorAssets = currentGallery.allAssets
|
|
1582
|
+
var targetIndex = 0
|
|
1583
|
+
if let idx = editorAssets.firstIndex(of: asset) {
|
|
1584
|
+
targetIndex = idx
|
|
1585
|
+
} else {
|
|
1586
|
+
editorAssets.insert(asset, at: 0)
|
|
1587
|
+
targetIndex = 0
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
editorVC.allAssets = editorAssets
|
|
1591
|
+
editorVC.currentIndex = targetIndex
|
|
1592
|
+
editorVC.selectedAssets = currentGallery.selectedAssets
|
|
1593
|
+
|
|
1594
|
+
editorVC.onSelectionToggled = { [weak currentGallery] (toggledAsset, isSelected) in
|
|
1595
|
+
guard let currentGallery = currentGallery else { return }
|
|
1596
|
+
if isSelected {
|
|
1597
|
+
if !currentGallery.selectedAssets.contains(toggledAsset) {
|
|
1598
|
+
currentGallery.selectedAssets.append(toggledAsset)
|
|
1599
|
+
currentGallery.selectedAssetsSet.insert(toggledAsset.localIdentifier)
|
|
1600
|
+
}
|
|
1601
|
+
} else {
|
|
1602
|
+
currentGallery.selectedAssetsSet.remove(toggledAsset.localIdentifier)
|
|
1603
|
+
currentGallery.selectedAssets.removeAll(where: { $0.localIdentifier == toggledAsset.localIdentifier })
|
|
1604
|
+
}
|
|
1605
|
+
currentGallery.refreshSelectionAfterEdit()
|
|
1606
|
+
currentGallery.updateSelectedCellNumbers()
|
|
1607
|
+
currentGallery.notifySelectionChanged()
|
|
1608
|
+
currentGallery.updateNavigationBarForSelection()
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
editorVC.onEditDeleted = { [weak currentGallery] asset in
|
|
1612
|
+
currentGallery?.editedImages.removeValue(forKey: asset.localIdentifier)
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
editorVC.modalPresentationStyle = .overFullScreen
|
|
1616
|
+
editorVC.singlePhotoMode = !isMultiSelect
|
|
1617
|
+
// selectionMode 'multi' + 최대 1장: 편집 화면에서 다른 사진을 선택하면 기존 선택과 교체
|
|
1618
|
+
editorVC.replaceSelectionWhenFull = currentGallery.forceMultiMode && currentGallery.maxSelection == 1
|
|
1619
|
+
|
|
1620
|
+
var initialCroppedImages: [Int: UIImage] = [:]
|
|
1621
|
+
for (i, a) in editorAssets.enumerated() {
|
|
1622
|
+
if let editedImg = currentGallery.editedImages[a.localIdentifier] {
|
|
1623
|
+
initialCroppedImages[i] = editedImg
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
editorVC.croppedImages = initialCroppedImages
|
|
1627
|
+
|
|
1628
|
+
if sourceFrame != .zero {
|
|
1629
|
+
self.editorTransitionDelegate = ImageEditorTransitionDelegate()
|
|
1630
|
+
self.editorTransitionDelegate?.sourceFrame = sourceFrame
|
|
1631
|
+
self.editorTransitionDelegate?.sourceImage = sourceImage
|
|
1632
|
+
|
|
1633
|
+
if let editedImg = currentGallery.editedImages[asset.localIdentifier] {
|
|
1634
|
+
self.editorTransitionDelegate?.uncroppedImage = editedImg
|
|
1635
|
+
self.editorTransitionDelegate?.asset = nil
|
|
1636
|
+
} else {
|
|
1637
|
+
let options = PHImageRequestOptions()
|
|
1638
|
+
options.deliveryMode = .fastFormat
|
|
1639
|
+
options.isSynchronous = true
|
|
1640
|
+
options.resizeMode = .fast
|
|
1641
|
+
var fastUncropped: UIImage? = nil
|
|
1642
|
+
PHImageManager.default().requestImage(for: asset, targetSize: CGSize(width: 400, height: 400), contentMode: .aspectFit, options: options) { img, _ in
|
|
1643
|
+
fastUncropped = img
|
|
1644
|
+
}
|
|
1645
|
+
self.editorTransitionDelegate?.uncroppedImage = fastUncropped ?? sourceImage
|
|
1646
|
+
self.editorTransitionDelegate?.asset = asset
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
var ratio = CGFloat(asset.pixelWidth) / CGFloat(max(1, asset.pixelHeight))
|
|
1650
|
+
if let editedImg = currentGallery.editedImages[asset.localIdentifier] {
|
|
1651
|
+
ratio = editedImg.size.width / max(1.0, editedImg.size.height)
|
|
1652
|
+
}
|
|
1653
|
+
self.editorTransitionDelegate?.assetAspectRatio = ratio
|
|
1654
|
+
self.editorTransitionDelegate?.frameProvider = { [weak currentGallery] currentAsset in
|
|
1655
|
+
return currentGallery?.frameForAsset(currentAsset)
|
|
1656
|
+
}
|
|
1657
|
+
// sticky: 편집 화면이 열려 있는 동안 지금 사진의 갤러리 칸을 비워 두고(넘기면 따라감), 닫히면 다시 보이게
|
|
1658
|
+
self.editorTransitionDelegate?.onCurrentAssetChanged = { [weak currentGallery] currentAsset in
|
|
1659
|
+
currentGallery?.setHiddenAsset(currentAsset, scrollIntoView: true)
|
|
1660
|
+
}
|
|
1661
|
+
self.editorTransitionDelegate?.onEditorClosed = { [weak currentGallery] in
|
|
1662
|
+
currentGallery?.setHiddenAsset(nil)
|
|
1663
|
+
}
|
|
1664
|
+
currentGallery.setHiddenAsset(asset)
|
|
1665
|
+
|
|
1666
|
+
editorVC.transitioningDelegate = self.editorTransitionDelegate
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
if let hex = currentGallery.themeColorHex, let color = UIColor(hexString: hex) {
|
|
1670
|
+
editorVC.themeColor = color
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
editorVC.onConfirmMulti = { [weak currentGallery] (assets, croppedImages, filterStates) in
|
|
1674
|
+
guard let currentGallery = currentGallery else { return }
|
|
1675
|
+
|
|
1676
|
+
editorVC.dismiss(animated: true) {
|
|
1677
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
1678
|
+
var newlyEditedAssets: [PHAsset] = []
|
|
1679
|
+
|
|
1680
|
+
for asset in assets {
|
|
1681
|
+
if let baseImage = croppedImages[asset] {
|
|
1682
|
+
var finalImage = baseImage
|
|
1683
|
+
if let state = filterStates[asset],
|
|
1684
|
+
state.filterId != "original",
|
|
1685
|
+
let filter = FilterManager.shared.filters.first(where: { $0.id == state.filterId }),
|
|
1686
|
+
let ciImage = CIImage(image: baseImage),
|
|
1687
|
+
let out = filter.apply(ciImage, state.intensity),
|
|
1688
|
+
let cgImg = FilterManager.shared.context.createCGImage(out, from: out.extent) {
|
|
1689
|
+
finalImage = UIImage(cgImage: cgImg, scale: baseImage.scale, orientation: baseImage.imageOrientation)
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
DispatchQueue.main.async {
|
|
1693
|
+
currentGallery.editedImages[asset.localIdentifier] = finalImage
|
|
1694
|
+
}
|
|
1695
|
+
newlyEditedAssets.append(asset)
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
DispatchQueue.main.async {
|
|
1700
|
+
for asset in newlyEditedAssets {
|
|
1701
|
+
if !currentGallery.selectedAssetsSet.contains(asset.localIdentifier) {
|
|
1702
|
+
if currentGallery.maxSelection == 0 || currentGallery.selectedAssets.count < currentGallery.maxSelection {
|
|
1703
|
+
currentGallery.selectedAssets.append(asset)
|
|
1704
|
+
currentGallery.selectedAssetsSet.insert(asset.localIdentifier)
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
currentGallery.updateSelectedCellNumbers()
|
|
1709
|
+
currentGallery.notifySelectionChanged()
|
|
1710
|
+
currentGallery.updateNavigationBarForSelection()
|
|
1711
|
+
currentGallery.collectionView.reloadData()
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// patch398: 편집 화면의 전송 버튼 — 편집본을 갤러리에 반영하고 선택한 사진을 바로 전송(편집 화면/갤러리 모두 닫힘)
|
|
1718
|
+
editorVC.onSendMulti = { [weak currentGallery] (assets, croppedImages, filterStates) in
|
|
1719
|
+
guard let currentGallery = currentGallery else { return }
|
|
1720
|
+
for asset in assets {
|
|
1721
|
+
guard var img = croppedImages[asset] else { continue }
|
|
1722
|
+
if let state = filterStates[asset],
|
|
1723
|
+
state.filterId != "original",
|
|
1724
|
+
let filter = FilterManager.shared.filters.first(where: { $0.id == state.filterId }),
|
|
1725
|
+
let ciImage = CIImage(image: img),
|
|
1726
|
+
let out = filter.apply(ciImage, state.intensity),
|
|
1727
|
+
let cgImg = FilterManager.shared.context.createCGImage(out, from: out.extent) {
|
|
1728
|
+
img = UIImage(cgImage: cgImg, scale: img.scale, orientation: img.imageOrientation)
|
|
1729
|
+
}
|
|
1730
|
+
currentGallery.editedImages[asset.localIdentifier] = img
|
|
1731
|
+
}
|
|
1732
|
+
// patch399: 편집 화면의 이미지가 갤러리 자리로 돌아가는 애니메이션이 끝난 뒤에 전송합니다.
|
|
1733
|
+
currentGallery.refreshThumbnails(for: assets)
|
|
1734
|
+
currentGallery.updateSelectedCellNumbers()
|
|
1735
|
+
editorVC.dismiss(animated: true) { [weak currentGallery] in
|
|
1736
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
|
|
1737
|
+
currentGallery?.sendSelectionFromEditor()
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
editorVC.onConfirm = { [weak self, weak galleryNav, weak currentGallery] (editedAsset: PHAsset?, croppedImage: UIImage?, filterState: FilterState, caption: String) in
|
|
1743
|
+
guard let self = self else { return }
|
|
1744
|
+
// 단일 편집 → 전송: 결과 전달 책임이 processEditorResult 로 넘어가므로, 갤러리를 닫기 "전에"
|
|
1745
|
+
// 가드를 세웁니다. 안 그러면 갤러리 viewDidDisappear 의 "스와이프로 닫힘" 안전장치가
|
|
1746
|
+
// 먼저 onImagesSelected([]) → reject("CANCELLED") 를 보내서, 편집된 사진이 JS 로 오지
|
|
1747
|
+
// 않았습니다 (autoCloseOnSelect + enableEditor 조합에서 사진이 안 들어오던 버그).
|
|
1748
|
+
currentGallery?.hasReturnedImages = true
|
|
1749
|
+
let navToDismiss = galleryNav ?? currentGallery
|
|
1750
|
+
let presenter = navToDismiss?.presentingViewController ?? navToDismiss
|
|
1751
|
+
presenter?.dismiss(animated: true) {
|
|
1752
|
+
self.processEditorResult(asset: editedAsset, croppedImage: croppedImage, filterState: filterState, caption: caption, galleryVC: currentGallery, resolve: resolve, reject: reject)
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
editorVC.onCancel = { [weak editorVC] in
|
|
1757
|
+
// 에디터만 닫고 갤러리의 선택 상태는 유지합니다.
|
|
1758
|
+
editorVC?.dismiss(animated: true, completion: nil)
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
var savedDetent: UISheetPresentationController.Detent.Identifier? = nil
|
|
1762
|
+
if #available(iOS 15.0, *) {
|
|
1763
|
+
savedDetent = galleryNav?.sheetPresentationController?.selectedDetentIdentifier
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
let presenterVC = currentGallery.presentedViewController ?? currentGallery
|
|
1767
|
+
presenterVC.present(editorVC, animated: true) {
|
|
1768
|
+
if #available(iOS 15.0, *) {
|
|
1769
|
+
if let saved = savedDetent {
|
|
1770
|
+
galleryNav?.sheetPresentationController?.selectedDetentIdentifier = saved
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
extension RNAttachmentKit: UIViewControllerTransitioningDelegate {
|
|
1778
|
+
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
1779
|
+
zoomAnimator.isPresenting = true
|
|
1780
|
+
return zoomAnimator
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
1784
|
+
zoomAnimator.isPresenting = false
|
|
1785
|
+
return zoomAnimator
|
|
1786
|
+
}
|
|
1787
|
+
}
|