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.
Files changed (166) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +327 -0
  3. package/android/CMakeLists.txt +31 -0
  4. package/android/build.gradle +121 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
  7. package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +1159 -0
  8. package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitPackage.kt +37 -0
  9. package/android/src/oldarch/java/com/rnattachmentkit/NativeRNAttachmentKitSpec.kt +19 -0
  10. package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +1142 -0
  11. package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitPackage.kt +19 -0
  12. package/ios/README.md +213 -0
  13. package/ios/RNAttachmentKit.m +132 -0
  14. package/ios/RNAttachmentKit.swift +1787 -0
  15. package/ios/RNAttachmentKit.xcframework/Info.plist +44 -0
  16. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
  17. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Base.lproj/LaunchScreen.storyboardc/01J-lp-oVM-view-Ze5-6b-2t3.nib +0 -0
  18. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Base.lproj/LaunchScreen.storyboardc/Info.plist +0 -0
  19. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Base.lproj/LaunchScreen.storyboardc/UIViewController-01J-lp-oVM.nib +0 -0
  20. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Headers/RNAttachmentKit-Swift.h +581 -0
  21. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Info.plist +0 -0
  22. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +34376 -0
  23. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +517 -0
  24. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  25. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftinterface +517 -0
  26. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/module.modulemap +3 -0
  27. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Pretendard-SemiBold.otf +0 -0
  28. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
  29. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_reflect~dark.webp +0 -0
  30. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_reflect~light.webp +0 -0
  31. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~dark.webp +0 -0
  32. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~light.webp +0 -0
  33. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~dark.webp +0 -0
  34. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~light.webp +0 -0
  35. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~dark.webp +0 -0
  36. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~light.webp +0 -0
  37. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~dark.webp +0 -0
  38. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~light.webp +0 -0
  39. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~dark.webp +0 -0
  40. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~light.webp +0 -0
  41. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~dark.webp +0 -0
  42. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~light.webp +0 -0
  43. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~dark.webp +0 -0
  44. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~light.webp +0 -0
  45. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~dark.webp +0 -0
  46. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~light.webp +0 -0
  47. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~dark.webp +0 -0
  48. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~light.webp +0 -0
  49. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~dark.webp +0 -0
  50. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~light.webp +0 -0
  51. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~dark.webp +0 -0
  52. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~light.webp +0 -0
  53. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
  54. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeRequirements +0 -0
  55. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +498 -0
  56. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
  57. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Assets.car +0 -0
  58. 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
  59. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Base.lproj/LaunchScreen.storyboardc/Info.plist +0 -0
  60. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Base.lproj/LaunchScreen.storyboardc/UIViewController-01J-lp-oVM.nib +0 -0
  61. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Headers/RNAttachmentKit-Swift.h +1158 -0
  62. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Info.plist +0 -0
  63. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +34376 -0
  64. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +517 -0
  65. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  66. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +517 -0
  67. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +34376 -0
  68. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +517 -0
  69. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  70. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +517 -0
  71. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/module.modulemap +3 -0
  72. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Pretendard-SemiBold.otf +0 -0
  73. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/RNAttachmentKit +0 -0
  74. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_crop_reflect~dark.webp +0 -0
  75. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_crop_reflect~light.webp +0 -0
  76. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~dark.webp +0 -0
  77. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~light.webp +0 -0
  78. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~dark.webp +0 -0
  79. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~light.webp +0 -0
  80. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~dark.webp +0 -0
  81. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~light.webp +0 -0
  82. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~dark.webp +0 -0
  83. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~light.webp +0 -0
  84. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~dark.webp +0 -0
  85. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~light.webp +0 -0
  86. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~dark.webp +0 -0
  87. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~light.webp +0 -0
  88. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~dark.webp +0 -0
  89. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~light.webp +0 -0
  90. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~dark.webp +0 -0
  91. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~light.webp +0 -0
  92. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~dark.webp +0 -0
  93. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~light.webp +0 -0
  94. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~dark.webp +0 -0
  95. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~light.webp +0 -0
  96. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~dark.webp +0 -0
  97. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~light.webp +0 -0
  98. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
  99. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeRequirements +0 -0
  100. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +553 -0
  101. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
  102. package/ios/RNAttachmentKitAssets/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
  103. package/ios/RNAttachmentKitAssets/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
  104. package/ios/RNAttachmentKitAssets/Assets.xcassets/Contents.json +6 -0
  105. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_back.imageset/Contents.json +24 -0
  106. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_back.imageset/ic_back_dark.png +0 -0
  107. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_back.imageset/ic_back_normal.png +0 -0
  108. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_delete.imageset/Contents.json +56 -0
  109. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_delete.imageset/delete_dark.webp +0 -0
  110. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_delete.imageset/delete_normal.webp +0 -0
  111. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_forward.imageset/Contents.json +56 -0
  112. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_forward.imageset/forward_dark.webp +0 -0
  113. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_forward.imageset/forward_normal.webp +0 -0
  114. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_reward.imageset/Contents.json +56 -0
  115. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_reward.imageset/reward_dark.webp +0 -0
  116. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_drawing_reward.imageset/reward_normal.webp +0 -0
  117. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_crop.imageset/Contents.json +52 -0
  118. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_crop.imageset/crop_dark.webp +0 -0
  119. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_crop.imageset/crop_normal.webp +0 -0
  120. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_drawing.imageset/Contents.json +52 -0
  121. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_drawing.imageset/drawing_dark.webp +0 -0
  122. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_drawing.imageset/drawing_normal.webp +0 -0
  123. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_effect.imageset/Contents.json +52 -0
  124. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_effect.imageset/effect_dark.webp +0 -0
  125. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_effect.imageset/effect_normal.webp +0 -0
  126. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_emoji.imageset/Contents.json +52 -0
  127. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_emoji.imageset/emoji_dark.webp +0 -0
  128. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_emoji.imageset/emoji_normal.webp +0 -0
  129. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_text.imageset/Contents.json +52 -0
  130. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_text.imageset/text_dark.webp +0 -0
  131. package/ios/RNAttachmentKitAssets/Assets.xcassets/ic_editor_text.imageset/text_norml.webp +0 -0
  132. package/ios/RNAttachmentKitAssets/Assets.xcassets/rotate_icon.imageset/Contents.json +32 -0
  133. package/ios/RNAttachmentKitAssets/Assets.xcassets/rotate_icon.imageset/rotate_dark.png +0 -0
  134. package/ios/RNAttachmentKitAssets/Assets.xcassets/rotate_icon.imageset/rotate_normal.png +0 -0
  135. package/ios/RNAttachmentKitAssets/Fonts/Pretendard-SemiBold.otf +0 -0
  136. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_reflect~dark.webp +0 -0
  137. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_reflect~light.webp +0 -0
  138. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_rotate~dark.webp +0 -0
  139. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_rotate~light.webp +0 -0
  140. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_delete~dark.webp +0 -0
  141. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_delete~light.webp +0 -0
  142. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_forward~dark.webp +0 -0
  143. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_forward~light.webp +0 -0
  144. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_reward~dark.webp +0 -0
  145. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_reward~light.webp +0 -0
  146. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_crop~dark.webp +0 -0
  147. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_crop~light.webp +0 -0
  148. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_drawing~dark.webp +0 -0
  149. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_drawing~light.webp +0 -0
  150. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_effect~dark.webp +0 -0
  151. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_effect~light.webp +0 -0
  152. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_emoji~dark.webp +0 -0
  153. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_emoji~light.webp +0 -0
  154. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_eraser~dark.webp +0 -0
  155. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_eraser~light.webp +0 -0
  156. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_mosaic~dark.webp +0 -0
  157. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_mosaic~light.webp +0 -0
  158. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_text~dark.webp +0 -0
  159. package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_text~light.webp +0 -0
  160. package/package.json +68 -0
  161. package/react-native-attachment-kit.podspec +51 -0
  162. package/react-native.config.js +14 -0
  163. package/src/NativeAttachmentKit.ts +26 -0
  164. package/src/index.d.ts +56 -0
  165. package/src/index.ts +359 -0
  166. package/src/types.ts +317 -0
@@ -0,0 +1,1142 @@
1
+ package com.rnattachmentkit
2
+
3
+ import android.app.Activity
4
+ import android.content.Intent
5
+ import android.graphics.Bitmap
6
+ import android.graphics.BitmapFactory
7
+ import android.net.Uri
8
+ import com.facebook.react.bridge.*
9
+ import com.facebook.react.module.annotations.ReactModule
10
+ import android.Manifest
11
+ import android.content.pm.PackageManager
12
+ import android.os.Build
13
+ import androidx.core.content.ContextCompat
14
+ import androidx.fragment.app.FragmentActivity
15
+ import com.facebook.react.modules.core.PermissionAwareActivity
16
+ import com.facebook.react.modules.core.PermissionListener
17
+ import kotlinx.coroutines.CoroutineScope
18
+ import kotlinx.coroutines.Dispatchers
19
+ import kotlinx.coroutines.launch
20
+ import kotlinx.coroutines.withContext
21
+ import java.io.*
22
+
23
+ @ReactModule(name = RNAttachmentKitModule.NAME)
24
+ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
25
+ NativeRNAttachmentKitSpec(reactContext), ActivityEventListener {
26
+
27
+ companion object {
28
+ const val NAME = "RNAttachmentKit"
29
+ private const val E_PICKER_CANCELLED = "E_PICKER_CANCELLED"
30
+ private const val E_NO_ACTIVITY = "E_NO_ACTIVITY"
31
+ private const val E_FAILED_TO_PICK = "E_FAILED_TO_PICK"
32
+ private const val REQUEST_CODE_IMAGE_PICKER = 3001
33
+ private const val REQUEST_CODE_IMAGE_EDITOR = 3002
34
+ }
35
+
36
+ private var pickImagesPromise: Promise? = null
37
+ private var editImagePromise: Promise? = null
38
+ private val moduleScope = CoroutineScope(Dispatchers.Main)
39
+ private var maxWidth: Int = 1024
40
+ private var maxHeight: Int = 1024
41
+ private var outputFormat: String = "webp" // Default to webp
42
+
43
+ @ReactMethod
44
+ fun addListener(eventName: String) {
45
+ // Keep: Required for RN built in Event Emitter Calls.
46
+ }
47
+
48
+ @ReactMethod
49
+ fun removeListeners(count: Int) {
50
+ // Keep: Required for RN built in Event Emitter Calls.
51
+ }
52
+
53
+ private val viewerWillCloseReceiver = object : android.content.BroadcastReceiver() {
54
+ override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
55
+ if (intent?.action == "com.rnattachmentkit.VIEWER_WILL_CLOSE") {
56
+ reactApplicationContext
57
+ .getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
58
+ .emit("onViewerWillClose", com.facebook.react.bridge.Arguments.createMap())
59
+ }
60
+ }
61
+ }
62
+
63
+ // 이미지 뷰어 상단바의 삭제를 사용자가 확정했을 때(ImageViewerOverlayView.handleDeleteTapped).
64
+ // iOS의 "onImageDeleted" 이벤트와 동일한 body(index, url)로 JS에 알립니다 - 실제 메시지 삭제는 RN이 담당.
65
+ private val imageDeletedReceiver = object : android.content.BroadcastReceiver() {
66
+ override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
67
+ if (intent?.action == "com.rnattachmentkit.IMAGE_DELETED") {
68
+ val params = Arguments.createMap()
69
+ params.putInt("index", intent.getIntExtra("index", 0))
70
+ params.putString("url", intent.getStringExtra("url") ?: "")
71
+ // patch405: 사용자가 고른 삭제 방식(deleteOptions 의 key). 선택지가 없었으면 생략.
72
+ intent.getStringExtra("option")?.let { params.putString("option", it) }
73
+ reactApplicationContext
74
+ .getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
75
+ .emit("onImageDeleted", params)
76
+ }
77
+ }
78
+ }
79
+
80
+ // 열기 애니메이션이 끝난 시점을 JS에 알리는 이벤트 (배경 스크롤 보정 타이밍용).
81
+ private val viewerOpenedReceiver = object : android.content.BroadcastReceiver() {
82
+ override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
83
+ if (intent?.action == "com.rnattachmentkit.VIEWER_OPENED") {
84
+ reactApplicationContext
85
+ .getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
86
+ .emit("onViewerOpened", com.facebook.react.bridge.Arguments.createMap())
87
+ }
88
+ }
89
+ }
90
+
91
+ init {
92
+ com.rnattachmentkit.EventEmitterHelper.reactContext = reactContext
93
+ reactContext.addActivityEventListener(this)
94
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactContext)
95
+ .registerReceiver(viewerWillCloseReceiver, android.content.IntentFilter("com.rnattachmentkit.VIEWER_WILL_CLOSE"))
96
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactContext)
97
+ .registerReceiver(viewerOpenedReceiver, android.content.IntentFilter("com.rnattachmentkit.VIEWER_OPENED"))
98
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactContext)
99
+ .registerReceiver(imageDeletedReceiver, android.content.IntentFilter("com.rnattachmentkit.IMAGE_DELETED"))
100
+ }
101
+
102
+ override fun getName(): String = NAME
103
+
104
+ companion object {
105
+ const val NAME = "RNAttachmentKit"
106
+ const val REQUEST_CODE_IMAGE_EDITOR = 1002
107
+
108
+ val defaultAnimationConfig = mapOf(
109
+ "galleryOpen" to 300,
110
+ "galleryClose" to 250,
111
+ "editorOpen" to 250,
112
+ "editorClose" to 200,
113
+ "viewerOpen" to 250,
114
+ "viewerClose" to 200
115
+ )
116
+ }
117
+
118
+ @ReactMethod
119
+ override fun init(licenseKey: String, promise: Promise) {
120
+ val result = LicenseManager.initialize(reactApplicationContext, licenseKey)
121
+ promise.resolve(result)
122
+ }
123
+
124
+ @ReactMethod
125
+ fun getDefaultAnimationConfig(promise: Promise) {
126
+ val config = com.facebook.react.bridge.Arguments.createMap().apply {
127
+ putInt("galleryOpen", defaultAnimationConfig["galleryOpen"] ?: 300)
128
+ putInt("galleryClose", defaultAnimationConfig["galleryClose"] ?: 250)
129
+ putInt("editorOpen", defaultAnimationConfig["editorOpen"] ?: 250)
130
+ putInt("editorClose", defaultAnimationConfig["editorClose"] ?: 200)
131
+ putInt("viewerOpen", defaultAnimationConfig["viewerOpen"] ?: 250)
132
+ putInt("viewerClose", defaultAnimationConfig["viewerClose"] ?: 200)
133
+ }
134
+ promise.resolve(config)
135
+ }
136
+
137
+ @ReactMethod
138
+ override fun openEditor(options: ReadableMap, promise: Promise) {
139
+ val activity = getCurrentActivity()
140
+ if (activity == null) {
141
+ promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
142
+ return
143
+ }
144
+
145
+ val uriStr = if (options.hasKey("uri")) options.getString("uri") else null
146
+ if (uriStr == null) {
147
+ promise.reject(E_FAILED_TO_PICK, "uri is required")
148
+ return
149
+ }
150
+
151
+ val themeColor = if (options.hasKey("themeColor")) options.getString("themeColor") else null
152
+
153
+ if (options.hasKey("maxWidth")) maxWidth = options.getInt("maxWidth")
154
+ if (options.hasKey("maxHeight")) maxHeight = options.getInt("maxHeight")
155
+
156
+ editImagePromise = promise
157
+
158
+ try {
159
+ val intent = ImageEditorActivity.createIntent(
160
+ activity = activity,
161
+ uris = listOf(Uri.parse(uriStr)),
162
+ startIndex = 0,
163
+ themeColor = themeColor,
164
+ singlePhotoMode = true
165
+ )
166
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
167
+ activity.startActivityForResult(intent, REQUEST_CODE_IMAGE_EDITOR)
168
+ }, 50)
169
+ } catch (e: Exception) {
170
+ promise.reject(E_FAILED_TO_PICK, "Failed to open image editor: ${e.message}", e)
171
+ editImagePromise = null
172
+ }
173
+ }
174
+
175
+ @ReactMethod
176
+ override fun openGallery(options: ReadableMap, promise: Promise) {
177
+ val activity = getCurrentActivity()
178
+
179
+ if (activity == null) {
180
+ promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
181
+ return
182
+ }
183
+
184
+ // Parse options
185
+ val autoCloseOnSelect = if (options.hasKey("autoCloseOnSelect")) {
186
+ options.getBoolean("autoCloseOnSelect")
187
+ } else false
188
+
189
+ // 선택 방식 (selectionMode: 'single' | 'multi', 생략 시 기존 규칙)
190
+ // - 'multi' : maxSelection 이 1 이어도 멀티와 똑같이 (autoCloseOnSelect 무시, 1 미만이면 1장)
191
+ // - 'single': 기존 단일 동작 (maxSelection 을 1로, autoCloseOnSelect 면 0)
192
+ // - 생략 : 기존 그대로 (autoCloseOnSelect → 0, 아니면 maxSelection, 기본 1)
193
+ val selectionMode = if (options.hasKey("selectionMode")) options.getString("selectionMode")?.lowercase() else null
194
+ val forceMultiMode = selectionMode == "multi"
195
+ val maxSelection = if (forceMultiMode) {
196
+ (if (options.hasKey("maxSelection")) options.getInt("maxSelection") else 1).coerceAtLeast(1)
197
+ } else if (autoCloseOnSelect) {
198
+ 0
199
+ } else if (selectionMode == "single") {
200
+ 1
201
+ } else if (options.hasKey("maxSelection")) {
202
+ options.getInt("maxSelection")
203
+ } else {
204
+ 1
205
+ }
206
+
207
+ val languageCode = if (options.hasKey("languageCode")) {
208
+ options.getString("languageCode") ?: "en"
209
+ } else {
210
+ "en"
211
+ }
212
+ // 편집기/크롭 등 다른 화면들은 TurboImagePickerConfig.languageCode(전역)로 로케일을 잡는데,
213
+ // 그동안 JS가 넘긴 languageCode를 여기에 반영하는 코드가 없어서 항상 기본값 "en"이었습니다.
214
+ TurboImagePickerConfig.languageCode = languageCode
215
+
216
+ val themeColor = if (options.hasKey("themeColor")) {
217
+ options.getString("themeColor")
218
+ } else {
219
+ null
220
+ }
221
+
222
+ maxWidth = if (options.hasKey("maxWidth")) {
223
+ val value = options.getInt("maxWidth")
224
+ if (value > 0) value else 1024
225
+ } else {
226
+ 1024
227
+ }
228
+
229
+ maxHeight = if (options.hasKey("maxHeight")) {
230
+ val value = options.getInt("maxHeight")
231
+ if (value > 0) value else 1024
232
+ } else {
233
+ 1024
234
+ }
235
+
236
+ outputFormat = if (options.hasKey("outputFormat")) {
237
+ options.getString("outputFormat")?.lowercase() ?: "webp"
238
+ } else if (options.hasKey("format")) {
239
+ options.getString("format")?.lowercase() ?: "webp"
240
+ } else {
241
+ "webp"
242
+ }
243
+
244
+ val enableEditor = if (options.hasKey("enableEditor")) {
245
+ options.getBoolean("enableEditor")
246
+ } else {
247
+ false
248
+ }
249
+
250
+ val profileMode = if (options.hasKey("profileMode")) {
251
+ options.getBoolean("profileMode")
252
+ } else {
253
+ false
254
+ }
255
+
256
+ val fragmentActivity = activity as? FragmentActivity
257
+ if (fragmentActivity == null) {
258
+ promise.reject(E_NO_ACTIVITY, "Activity is not a FragmentActivity")
259
+ return
260
+ }
261
+
262
+ pickImagesPromise = promise
263
+
264
+ val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
265
+ Manifest.permission.READ_MEDIA_IMAGES
266
+ } else {
267
+ Manifest.permission.READ_EXTERNAL_STORAGE
268
+ }
269
+
270
+ val showSheet = {
271
+ fragmentActivity.runOnUiThread {
272
+ try {
273
+ val bottomSheet = ImagePickerBottomSheet.newInstance(
274
+ maxSelection, languageCode, enableEditor, profileMode, maxWidth, maxHeight, themeColor,
275
+ forceMultiMode = forceMultiMode
276
+ )
277
+ bottomSheet.setOnImagesSelectedListener { uris ->
278
+ val result = WritableNativeArray()
279
+ for (uri in uris) {
280
+ result.pushString(uri.toString())
281
+ }
282
+ handleSelectedImages(result, pickImagesPromise)
283
+ pickImagesPromise = null
284
+ }
285
+ bottomSheet.setOnCancelledListener {
286
+ pickImagesPromise?.reject(E_PICKER_CANCELLED, "User cancelled image picker")
287
+ pickImagesPromise = null
288
+ }
289
+
290
+ val existingFragment = fragmentActivity.supportFragmentManager.findFragmentByTag("ImagePickerBottomSheet")
291
+ existingFragment?.let {
292
+ fragmentActivity.supportFragmentManager.beginTransaction().remove(it).commitAllowingStateLoss()
293
+ }
294
+
295
+ fragmentActivity.supportFragmentManager.beginTransaction()
296
+ .add(bottomSheet, "ImagePickerBottomSheet")
297
+ .commitAllowingStateLoss()
298
+
299
+ } catch (e: Exception) {
300
+ pickImagesPromise?.reject(E_FAILED_TO_PICK, "Failed to open image picker: ${e.message}", e)
301
+ pickImagesPromise = null
302
+ }
303
+ }
304
+ }
305
+
306
+ if (ContextCompat.checkSelfPermission(fragmentActivity, permission) == PackageManager.PERMISSION_GRANTED) {
307
+ showSheet()
308
+ } else {
309
+ val permissionAwareActivity = fragmentActivity as? PermissionAwareActivity
310
+ if (permissionAwareActivity == null) {
311
+ promise.reject(E_NO_ACTIVITY, "Activity is not PermissionAwareActivity")
312
+ pickImagesPromise = null
313
+ return
314
+ }
315
+
316
+ val permissionsToRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && permission == Manifest.permission.READ_MEDIA_IMAGES) {
317
+ if (ContextCompat.checkSelfPermission(fragmentActivity, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED) {
318
+ showSheet()
319
+ return
320
+ }
321
+ arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
322
+ } else {
323
+ arrayOf(permission)
324
+ }
325
+
326
+ permissionAwareActivity.requestPermissions(permissionsToRequest, 100, object : PermissionListener {
327
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray): Boolean {
328
+ if (requestCode == 100 && grantResults != null) {
329
+ val isGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
330
+ grantResults.isNotEmpty() && (
331
+ grantResults[0] == PackageManager.PERMISSION_GRANTED ||
332
+ (grantResults.size > 1 && grantResults[1] == PackageManager.PERMISSION_GRANTED)
333
+ )
334
+ } else {
335
+ grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
336
+ }
337
+
338
+ if (isGranted) {
339
+ showSheet()
340
+ } else {
341
+ pickImagesPromise?.reject("E_PERMISSION_DENIED", "Permission denied")
342
+ pickImagesPromise = null
343
+ }
344
+ }
345
+ return true
346
+ }
347
+ })
348
+ }
349
+ }
350
+
351
+ @ReactMethod
352
+ override fun updateSourceRect(options: ReadableMap, promise: Promise) {
353
+ var startX = -1f
354
+ var startY = -1f
355
+ var startWidth = -1f
356
+ var startHeight = -1f
357
+
358
+ if (options.hasKey("x")) {
359
+ startX = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("x").toFloat())
360
+ }
361
+ if (options.hasKey("y")) {
362
+ startY = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("y").toFloat())
363
+ }
364
+ if (options.hasKey("width")) {
365
+ startWidth = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("width").toFloat())
366
+ }
367
+ if (options.hasKey("height")) {
368
+ startHeight = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("height").toFloat())
369
+ }
370
+
371
+ // 브로드캐스트가 유실되는 타이밍(Fragment의 onViewCreated 전) 대비, sticky 값도 함께 저장.
372
+ // (useDialogViewer/useOverlayViewer 중 실제 사용 중인 쪽만 값을 읽으므로 둘 다 갱신해도 무해함)
373
+ ImageViewerDialogFragment.updatePendingCoordinates(startX, startY, startWidth, startHeight)
374
+ ImageViewerOverlayView.updatePendingCoordinates(startX, startY, startWidth, startHeight)
375
+
376
+ val intent = android.content.Intent("com.rnattachmentkit.UPDATE_COORDINATES")
377
+ intent.putExtra("startX", startX)
378
+ intent.putExtra("startY", startY)
379
+ intent.putExtra("startWidth", startWidth)
380
+ intent.putExtra("startHeight", startHeight)
381
+ // 그동안 sourceBorderCorners(어느 모서리를 둥글릴지)가 여기서 전달되지 않아서, 그룹 이미지에서
382
+ // 스와이프로 다른 셀(예: 오른쪽 위 모서리 이미지)로 이동해도 마스크의 라운드 코너 패턴이
383
+ // 처음 열었던 이미지의 것으로 고정된 채 안 바뀌는 문제가 있었습니다. JS가 onPageSelected에서
384
+ // 매번 새로 계산해서 보내주는 sourceBorderCorners를 브로드캐스트에 실어 보내도록 수정합니다.
385
+ if (options.hasKey("sourceBorderCorners")) {
386
+ val cornersArray = options.getArray("sourceBorderCorners")
387
+ if (cornersArray != null) {
388
+ val corners = java.util.ArrayList<String>()
389
+ for (i in 0 until cornersArray.size()) {
390
+ cornersArray.getString(i)?.let { corners.add(it) }
391
+ }
392
+ intent.putStringArrayListExtra("sourceBorderCorners", corners)
393
+ }
394
+ }
395
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext).sendBroadcast(intent)
396
+ promise.resolve(null)
397
+ }
398
+
399
+ @ReactMethod
400
+ override fun closeGallery(promise: Promise) {
401
+ val currentActivity = currentActivity
402
+ if (currentActivity != null) {
403
+ val intent = android.content.Intent("com.rnattachmentkit.CLOSE_GALLERY")
404
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(currentActivity).sendBroadcast(intent)
405
+ promise.resolve(true)
406
+ } else {
407
+ promise.resolve(false)
408
+ }
409
+ }
410
+
411
+ @ReactMethod
412
+ override fun openViewer(options: ReadableMap, promise: Promise) {
413
+ val activity = getCurrentActivity()
414
+ if (activity == null) {
415
+ promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
416
+ return
417
+ }
418
+
419
+ if (!options.hasKey("images")) {
420
+ promise.reject(E_FAILED_TO_PICK, "images array is required")
421
+ return
422
+ }
423
+
424
+ val imagesArray = options.getArray("images")
425
+ if (imagesArray == null || imagesArray.size() == 0) {
426
+ promise.reject(E_FAILED_TO_PICK, "images array cannot be empty")
427
+ return
428
+ }
429
+
430
+ val images = ArrayList<String>()
431
+ for (i in 0 until imagesArray.size()) {
432
+ val str = imagesArray.getString(i)
433
+ if (str != null) {
434
+ images.add(str)
435
+ }
436
+ }
437
+
438
+ // cacheFiles: images 와 같은 순서의 앱 캐시 파일 경로 (null → 빈 문자열). 뷰어 전체(모든 방식)에 적용.
439
+ val cacheFiles = ArrayList<String>()
440
+ if (options.hasKey("cacheFiles") && !options.isNull("cacheFiles")) {
441
+ val arr = options.getArray("cacheFiles")
442
+ if (arr != null) {
443
+ for (i in 0 until arr.size()) {
444
+ cacheFiles.add(if (arr.isNull(i)) "" else (arr.getString(i) ?: ""))
445
+ }
446
+ }
447
+ }
448
+ ViewerCacheFiles.register(images, if (cacheFiles.isEmpty()) null else cacheFiles)
449
+
450
+ val initialIndex = if (options.hasKey("initialIndex")) options.getInt("initialIndex") else 0
451
+ val themeColor = if (options.hasKey("themeColor")) options.getString("themeColor") else "#FF6B35"
452
+ val title = if (options.hasKey("title")) options.getString("title") else null
453
+ val animationType = if (options.hasKey("animationType")) options.getString("animationType") else null
454
+ // ⚠️ 뷰어가 항상 영어로만 나오던 원인: JS는 init()에서 설정한 기본 언어를 openViewer 옵션의
455
+ // languageCode로 매번 같이 넘기는데, 안드로이드 쪽에서는 그 값을 한 번도 읽지 않았고
456
+ // ImageViewerOverlayView는 TurboImagePickerConfig.attachBaseContext()(전역 languageCode,
457
+ // 기본값 "en")로 로케일을 강제하고 있었습니다. 여기서 반영해야 뷰어의 모든 문구가
458
+ // 설정된 언어를 따릅니다.
459
+ val viewerLanguageCode = if (options.hasKey("languageCode")) (options.getString("languageCode") ?: "en") else "en"
460
+ TurboImagePickerConfig.languageCode = viewerLanguageCode
461
+
462
+ val openDuration = if (options.hasKey("openDuration")) options.getInt("openDuration") else defaultAnimationConfig["viewerOpen"] ?: 250
463
+ val closeDuration = if (options.hasKey("closeDuration")) options.getInt("closeDuration") else defaultAnimationConfig["viewerClose"] ?: 200
464
+
465
+ var startX = -1f
466
+ var startY = -1f
467
+ var startWidth = -1f
468
+ var startHeight = -1f
469
+
470
+ if (options.hasKey("sourceRect")) {
471
+ val sourceRect = options.getMap("sourceRect")
472
+ if (sourceRect != null) {
473
+ startX = if (sourceRect.hasKey("x")) com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(sourceRect.getDouble("x").toFloat()) else -1f
474
+ startY = if (sourceRect.hasKey("y")) com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(sourceRect.getDouble("y").toFloat()) else -1f
475
+ startWidth = if (sourceRect.hasKey("width")) com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(sourceRect.getDouble("width").toFloat()) else -1f
476
+ startHeight = if (sourceRect.hasKey("height")) com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(sourceRect.getDouble("height").toFloat()) else -1f
477
+ }
478
+ }
479
+
480
+ val useDialogViewer = options.hasKey("useDialogViewer") && options.getBoolean("useDialogViewer")
481
+ val useOverlayViewer = options.hasKey("useOverlayViewer") && options.getBoolean("useOverlayViewer")
482
+
483
+ try {
484
+ if (useOverlayViewer) {
485
+ val args = android.os.Bundle().apply {
486
+ putStringArrayList(ImageViewerActivity.EXTRA_IMAGES, images)
487
+ putInt(ImageViewerActivity.EXTRA_INITIAL_INDEX, initialIndex)
488
+ putString("EXTRA_THEME_COLOR", themeColor)
489
+ if (title != null) {
490
+ putString(ImageViewerActivity.EXTRA_TITLE, title)
491
+ }
492
+ if (animationType != null) {
493
+ putString("animationType", animationType)
494
+ }
495
+ if (options.hasKey("closeAnimationType")) {
496
+ putString("closeAnimationType", options.getString("closeAnimationType"))
497
+ }
498
+ if (startX != -1f) {
499
+ putFloat("startX", startX)
500
+ putFloat("startY", startY)
501
+ putFloat("startWidth", startWidth)
502
+ putFloat("startHeight", startHeight)
503
+ }
504
+ if (options.hasKey("sourceBorderRadius")) {
505
+ putFloat("sourceBorderRadius", com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("sourceBorderRadius").toFloat()))
506
+ }
507
+ if (options.hasKey("sourceBorderCorners")) {
508
+ val cornersArray = options.getArray("sourceBorderCorners")
509
+ if (cornersArray != null) {
510
+ val corners = ArrayList<String>()
511
+ for (i in 0 until cornersArray.size()) {
512
+ cornersArray.getString(i)?.let { corners.add(it) }
513
+ }
514
+ putStringArrayList("sourceBorderCorners", corners)
515
+ }
516
+ }
517
+ // sourceBackgroundColor가 그동안 어느 뷰어 분기에서도 전달되지 않아
518
+ // mMaskView(닫기/열기 애니메이션 중 원본 썸네일을 가리는 마스크)가 항상 생성되지
519
+ // 않던 문제를 수정: JS의 sourceBackgroundColor(hex 문자열)를 그대로 전달합니다.
520
+ if (options.hasKey("sourceBackgroundColor")) {
521
+ putString("sourceBackgroundColor", options.getString("sourceBackgroundColor"))
522
+ }
523
+ // placeholderImages: 타입/뷰어 코드에는 있었지만 브릿지에서 한 번도 전달되지 않았던
524
+ // 필드. 네이티브 뷰어가 onlyRetrieveFromCache(true)로 첫 프레임을 즉시 그리려 할 때
525
+ // 쓰는 저해상도 대체 이미지 목록입니다(원본 다운로드 전 임시 표시용).
526
+ if (options.hasKey("placeholderImages")) {
527
+ val placeholderArray = options.getArray("placeholderImages")
528
+ if (placeholderArray != null) {
529
+ val placeholders = ArrayList<String>()
530
+ for (i in 0 until placeholderArray.size()) {
531
+ placeholders.add(placeholderArray.getString(i) ?: "")
532
+ }
533
+ putStringArrayList("placeholderImages", placeholders)
534
+ }
535
+ }
536
+ // 상단바 저장/공유/삭제 버튼과 그 문구들 (iOS와 동일한 옵션 키). 문구를 안 넘기면
537
+ // 네이티브 쪽 로케일 리소스(values-ko/ja/zh, 그 외 en)가 자동 적용됩니다.
538
+ putBoolean("showActionButtons", !options.hasKey("showActionButtons") || options.getBoolean("showActionButtons"))
539
+ for (key in listOf(
540
+ "saveAllText", "saveOneText", "shareAllText", "shareOneText", "actionCancelText",
541
+ "deleteConfirmTitle", "deleteConfirmMessage", "deleteConfirmGroupMessage", "deleteButtonText"
542
+ )) {
543
+ if (options.hasKey(key)) {
544
+ options.getString(key)?.let { putString(key, it) }
545
+ }
546
+ }
547
+ // patch405: 삭제 방식 선택지 [{ key, label, destructive }] → 나란한 배열 3개로 전달
548
+ if (options.hasKey("deleteOptions") && !options.isNull("deleteOptions")) {
549
+ val arr = options.getArray("deleteOptions")
550
+ if (arr != null) {
551
+ val keys = ArrayList<String>()
552
+ val labels = ArrayList<String>()
553
+ val destructive = ArrayList<Boolean>()
554
+ for (i in 0 until arr.size()) {
555
+ val m = arr.getMap(i) ?: continue
556
+ val k = if (m.hasKey("key")) m.getString("key") else null
557
+ val l = if (m.hasKey("label")) m.getString("label") else null
558
+ if (k.isNullOrEmpty() || l.isNullOrEmpty()) continue
559
+ keys.add(k)
560
+ labels.add(l)
561
+ destructive.add(m.hasKey("destructive") && !m.isNull("destructive") && m.getBoolean("destructive"))
562
+ }
563
+ if (keys.isNotEmpty()) {
564
+ putStringArrayList("deleteOptionKeys", keys)
565
+ putStringArrayList("deleteOptionLabels", labels)
566
+ putBooleanArray("deleteOptionDestructive", destructive.toBooleanArray())
567
+ }
568
+ }
569
+ }
570
+ }
571
+ // 이전 세션에서 남은 sticky 좌표가 이번 오픈에 잘못 적용되지 않도록 초기화.
572
+ ImageViewerOverlayView.clearPendingCoordinates()
573
+ activity.runOnUiThread {
574
+ ImageViewerOverlayView.present(activity, args)
575
+ }
576
+ promise.resolve(null)
577
+ return
578
+ }
579
+
580
+ if (useDialogViewer && activity is androidx.fragment.app.FragmentActivity) {
581
+ val args = android.os.Bundle().apply {
582
+ putStringArrayList(ImageViewerActivity.EXTRA_IMAGES, images)
583
+ putInt(ImageViewerActivity.EXTRA_INITIAL_INDEX, initialIndex)
584
+ putString("EXTRA_THEME_COLOR", themeColor)
585
+ if (title != null) {
586
+ putString(ImageViewerActivity.EXTRA_TITLE, title)
587
+ }
588
+ if (animationType != null) {
589
+ putString("animationType", animationType)
590
+ }
591
+ if (options.hasKey("closeAnimationType")) {
592
+ putString("closeAnimationType", options.getString("closeAnimationType"))
593
+ }
594
+ if (startX != -1f) {
595
+ putFloat("startX", startX)
596
+ putFloat("startY", startY)
597
+ putFloat("startWidth", startWidth)
598
+ putFloat("startHeight", startHeight)
599
+ }
600
+ if (options.hasKey("sourceBorderRadius")) {
601
+ putFloat("sourceBorderRadius", com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("sourceBorderRadius").toFloat()))
602
+ }
603
+ if (options.hasKey("sourceBorderCorners")) {
604
+ val cornersArray = options.getArray("sourceBorderCorners")
605
+ if (cornersArray != null) {
606
+ val corners = ArrayList<String>()
607
+ for (i in 0 until cornersArray.size()) {
608
+ cornersArray.getString(i)?.let { corners.add(it) }
609
+ }
610
+ putStringArrayList("sourceBorderCorners", corners)
611
+ }
612
+ }
613
+ // sourceBackgroundColor가 그동안 어느 뷰어 분기에서도 전달되지 않아
614
+ // mMaskView(닫기/열기 애니메이션 중 원본 썸네일을 가리는 마스크)가 항상 생성되지
615
+ // 않던 문제를 수정: JS의 sourceBackgroundColor(hex 문자열)를 그대로 전달합니다.
616
+ if (options.hasKey("sourceBackgroundColor")) {
617
+ putString("sourceBackgroundColor", options.getString("sourceBackgroundColor"))
618
+ }
619
+ // placeholderImages: 타입/뷰어 코드에는 있었지만 브릿지에서 한 번도 전달되지 않았던
620
+ // 필드. 네이티브 뷰어가 onlyRetrieveFromCache(true)로 첫 프레임을 즉시 그리려 할 때
621
+ // 쓰는 저해상도 대체 이미지 목록입니다(원본 다운로드 전 임시 표시용).
622
+ if (options.hasKey("placeholderImages")) {
623
+ val placeholderArray = options.getArray("placeholderImages")
624
+ if (placeholderArray != null) {
625
+ val placeholders = ArrayList<String>()
626
+ for (i in 0 until placeholderArray.size()) {
627
+ placeholders.add(placeholderArray.getString(i) ?: "")
628
+ }
629
+ putStringArrayList("placeholderImages", placeholders)
630
+ }
631
+ }
632
+ }
633
+ // 이전 세션에서 남은 sticky 좌표가 이번 오픈에 잘못 적용되지 않도록 초기화.
634
+ ImageViewerDialogFragment.clearPendingCoordinates()
635
+ activity.runOnUiThread {
636
+ ImageViewerDialogFragment.newInstance(args)
637
+ .show(activity.supportFragmentManager, "image_viewer_dialog")
638
+ }
639
+ promise.resolve(null)
640
+ return
641
+ }
642
+
643
+ val intent = Intent(activity, ImageViewerActivity::class.java).apply {
644
+ putStringArrayListExtra(ImageViewerActivity.EXTRA_IMAGES, images)
645
+ putExtra(ImageViewerActivity.EXTRA_INITIAL_INDEX, initialIndex)
646
+ putExtra("EXTRA_THEME_COLOR", themeColor)
647
+ if (title != null) {
648
+ putExtra(ImageViewerActivity.EXTRA_TITLE, title)
649
+ }
650
+ if (animationType != null) {
651
+ putExtra("animationType", animationType)
652
+ }
653
+ if (options.hasKey("closeAnimationType")) {
654
+ putExtra("closeAnimationType", options.getString("closeAnimationType"))
655
+ }
656
+ putExtra("openDuration", openDuration)
657
+ putExtra("closeDuration", closeDuration)
658
+ if (startX != -1f) {
659
+ putExtra("startX", startX)
660
+ putExtra("startY", startY)
661
+ putExtra("startWidth", startWidth)
662
+ putExtra("startHeight", startHeight)
663
+ }
664
+ if (options.hasKey("sourceBorderRadius")) {
665
+ putExtra("sourceBorderRadius", com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("sourceBorderRadius").toFloat()))
666
+ }
667
+ if (options.hasKey("sourceBorderCorners")) {
668
+ val cornersArray = options.getArray("sourceBorderCorners")
669
+ if (cornersArray != null) {
670
+ val corners = java.util.ArrayList<String>()
671
+ for (i in 0 until cornersArray.size()) {
672
+ cornersArray.getString(i)?.let { corners.add(it) }
673
+ }
674
+ putStringArrayListExtra("sourceBorderCorners", corners)
675
+ }
676
+ }
677
+ if (options.hasKey("sourceBackgroundColor")) {
678
+ putExtra("sourceBackgroundColor", options.getString("sourceBackgroundColor"))
679
+ }
680
+ if (options.hasKey("placeholderImages")) {
681
+ val placeholderArray = options.getArray("placeholderImages")
682
+ if (placeholderArray != null) {
683
+ val placeholders = ArrayList<String>()
684
+ for (i in 0 until placeholderArray.size()) {
685
+ placeholders.add(placeholderArray.getString(i) ?: "")
686
+ }
687
+ putStringArrayListExtra("placeholderImages", placeholders)
688
+ }
689
+ }
690
+ }
691
+ activity.startActivity(intent)
692
+ promise.resolve(null)
693
+ } catch (e: Exception) {
694
+ promise.reject(E_FAILED_TO_PICK, "Failed to open image viewer: ${e.message}", e)
695
+ }
696
+ }
697
+
698
+ override fun onActivityResult(
699
+ activity: Activity,
700
+ requestCode: Int,
701
+ resultCode: Int,
702
+ data: Intent?
703
+ ) {
704
+ if (requestCode == REQUEST_CODE_IMAGE_EDITOR) {
705
+ val promise = editImagePromise ?: return
706
+ editImagePromise = null
707
+
708
+ if (resultCode != Activity.RESULT_OK || data == null) {
709
+ promise.reject(E_PICKER_CANCELLED, "User cancelled image editor")
710
+ return
711
+ }
712
+
713
+ val uriStrings = data.getStringArrayListExtra(ImageEditorActivity.EXTRA_RESULT_URIS)
714
+ if (uriStrings == null || uriStrings.isEmpty()) {
715
+ promise.reject(E_PICKER_CANCELLED, "User cancelled image editor")
716
+ return
717
+ }
718
+
719
+ val uris = uriStrings.mapNotNull { uriString ->
720
+ try { Uri.parse(uriString) } catch (e: Exception) { null }
721
+ }
722
+
723
+ if (uris.isEmpty()) {
724
+ promise.reject(E_FAILED_TO_PICK, "Failed to parse image URIs")
725
+ return
726
+ }
727
+
728
+ moduleScope.launch {
729
+ try {
730
+ val results = processImages(uris)
731
+ if (results.isNotEmpty()) {
732
+ promise.resolve(results[0])
733
+ } else {
734
+ promise.reject(E_FAILED_TO_PICK, "Failed to process image")
735
+ }
736
+ } catch (e: Exception) {
737
+ promise.reject(E_FAILED_TO_PICK, "Failed to process image: ${e.message}", e)
738
+ }
739
+ }
740
+ return
741
+ }
742
+
743
+
744
+ }
745
+
746
+ override fun onNewIntent(intent: Intent) {
747
+ // Not needed for this module
748
+ }
749
+
750
+ private fun handleSelectedImages(uriStrings: ReadableArray, promise: Promise?) {
751
+ val actualPromise = promise ?: return
752
+
753
+ // Convert string URIs to Uri objects
754
+ val uris = mutableListOf<Uri>()
755
+ for (i in 0 until uriStrings.size()) {
756
+ val uriString = uriStrings.getString(i)
757
+ if (uriString != null) {
758
+ try {
759
+ uris.add(Uri.parse(uriString))
760
+ } catch (e: Exception) {
761
+ // Ignore
762
+ }
763
+ }
764
+ }
765
+
766
+ if (uris.isEmpty()) {
767
+ actualPromise.reject(E_FAILED_TO_PICK, "Failed to parse image URIs")
768
+ return
769
+ }
770
+
771
+ // Process images asynchronously
772
+ val eventMap = com.facebook.react.bridge.WritableNativeMap()
773
+ eventMap.putInt("selectedCount", uris.size)
774
+ eventMap.putInt("maxSelection", 10)
775
+ try {
776
+ reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
777
+ .emit("onSelectionChange", eventMap)
778
+ } catch (e: Exception) {
779
+ // Ignore
780
+ }
781
+
782
+ moduleScope.launch {
783
+ try {
784
+ val results = processImages(uris)
785
+ val array = com.facebook.react.bridge.Arguments.createArray()
786
+ results.forEach { array.pushMap(it) }
787
+ actualPromise.resolve(array)
788
+ } catch (e: Exception) {
789
+ actualPromise.reject(E_FAILED_TO_PICK, "Failed to process images: ${e.message}", e)
790
+ }
791
+ }
792
+ }
793
+
794
+ private suspend fun processImages(uris: List<Uri>): List<com.facebook.react.bridge.WritableMap> = withContext(Dispatchers.IO) {
795
+ val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
796
+ val contentResolver = reactApplicationContext.contentResolver
797
+ val totalCount = uris.size
798
+ var currentIndex = 0
799
+
800
+ for (uri in uris) {
801
+ try {
802
+ // 1. Get EXIF Orientation
803
+ var orientation = android.media.ExifInterface.ORIENTATION_NORMAL
804
+ try {
805
+ contentResolver.openInputStream(uri)?.use { exifInputStream ->
806
+ val exif = android.media.ExifInterface(exifInputStream)
807
+ orientation = exif.getAttributeInt(android.media.ExifInterface.TAG_ORIENTATION, android.media.ExifInterface.ORIENTATION_NORMAL)
808
+ }
809
+ } catch (e: Exception) {}
810
+
811
+ contentResolver.openInputStream(uri)?.use { inputStream ->
812
+ // Get original image dimensions
813
+ val originalOptions = BitmapFactory.Options().apply {
814
+ inJustDecodeBounds = true
815
+ }
816
+ BitmapFactory.decodeStream(inputStream, null, originalOptions)
817
+
818
+ var originalWidth = originalOptions.outWidth
819
+ var originalHeight = originalOptions.outHeight
820
+
821
+ // Swap width and height if rotated by 90 or 270 degrees
822
+ if (orientation == android.media.ExifInterface.ORIENTATION_ROTATE_90 || orientation == android.media.ExifInterface.ORIENTATION_ROTATE_270) {
823
+ val temp = originalWidth
824
+ originalWidth = originalHeight
825
+ originalHeight = temp
826
+ }
827
+
828
+ // Load and resize image
829
+ contentResolver.openInputStream(uri)?.use { resizedInputStream ->
830
+ // Initial decode for sizing
831
+ val decodeOptions = BitmapFactory.Options().apply {
832
+ inSampleSize = calculateInSampleSize(originalWidth, originalHeight, maxWidth, maxHeight)
833
+ }
834
+ var bitmap = BitmapFactory.decodeStream(resizedInputStream, null, decodeOptions)
835
+
836
+ if (bitmap != null) {
837
+ // Apply EXIF Rotation
838
+ val matrix = android.graphics.Matrix()
839
+ when (orientation) {
840
+ android.media.ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
841
+ android.media.ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
842
+ android.media.ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
843
+ android.media.ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1f, 1f)
844
+ android.media.ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1f, -1f)
845
+ }
846
+
847
+ if (!matrix.isIdentity) {
848
+ val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
849
+ if (rotatedBitmap != bitmap) {
850
+ bitmap.recycle()
851
+ bitmap = rotatedBitmap
852
+ }
853
+ }
854
+
855
+ // Precise resize if needed
856
+ val finalBitmap = if (bitmap.width > maxWidth || bitmap.height > maxHeight) {
857
+ resizeBitmap(bitmap, maxWidth, maxHeight)
858
+ } else {
859
+ bitmap
860
+ }
861
+
862
+ val processedBitmap = ImageProcessor.applyWatermarkIfNeeded(reactApplicationContext, finalBitmap)
863
+
864
+ // Determine format and extension
865
+ val compressFormat: Bitmap.CompressFormat
866
+ val extension: String
867
+ val mimeType: String
868
+
869
+ when (outputFormat) {
870
+ "png" -> {
871
+ compressFormat = Bitmap.CompressFormat.PNG
872
+ extension = "png"
873
+ mimeType = "image/png"
874
+ }
875
+ "jpg", "jpeg" -> {
876
+ compressFormat = Bitmap.CompressFormat.JPEG
877
+ extension = "jpg"
878
+ mimeType = "image/jpeg"
879
+ }
880
+ else -> { // webp
881
+ compressFormat = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
882
+ Bitmap.CompressFormat.WEBP_LOSSY
883
+ } else {
884
+ @Suppress("DEPRECATION")
885
+ Bitmap.CompressFormat.WEBP
886
+ }
887
+ extension = "webp"
888
+ mimeType = "image/webp"
889
+ }
890
+ }
891
+
892
+ // Save to temporary file
893
+ val tempFile = File.createTempFile("turbo_edited_${System.currentTimeMillis()}_", ".$extension", reactApplicationContext.cacheDir)
894
+ FileOutputStream(tempFile).use { out ->
895
+ processedBitmap.compress(compressFormat, 85, out)
896
+ }
897
+
898
+ val finalWidth = processedBitmap.width
899
+ val finalHeight = processedBitmap.height
900
+
901
+ // Cleanup
902
+ if (bitmap != processedBitmap) {
903
+ bitmap.recycle()
904
+ }
905
+ if (finalBitmap != processedBitmap && finalBitmap != bitmap) {
906
+ finalBitmap.recycle()
907
+ }
908
+ processedBitmap.recycle()
909
+
910
+ val createImageInfo = {
911
+ WritableNativeMap().apply {
912
+ putString("uri", Uri.fromFile(tempFile).toString())
913
+ putInt("width", finalWidth)
914
+ putInt("height", finalHeight)
915
+ putString("type", mimeType)
916
+ putString("fileName", tempFile.name)
917
+ putString("fileExtension", extension)
918
+ putDouble("fileSize", tempFile.length().toDouble())
919
+ putString("originalUri", uri.toString())
920
+ putInt("originalWidth", originalWidth)
921
+ putInt("originalHeight", originalHeight)
922
+ }
923
+ }
924
+ val imageInfo = createImageInfo()
925
+ results.add(imageInfo)
926
+
927
+ try {
928
+ val eventMap = WritableNativeMap().apply {
929
+ putInt("total", totalCount)
930
+ putInt("index", currentIndex)
931
+ putMap("image", createImageInfo())
932
+ }
933
+ reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
934
+ .emit("onImageProcessed", eventMap)
935
+ } catch (e: Exception) { }
936
+ }
937
+ }
938
+ }
939
+ currentIndex++
940
+ } catch (e: Exception) {
941
+ currentIndex++
942
+ // Skip failed images
943
+ continue
944
+ }
945
+ }
946
+
947
+ results
948
+ }
949
+
950
+ private fun calculateInSampleSize(
951
+ reqWidth: Int,
952
+ reqHeight: Int,
953
+ maxWidth: Int,
954
+ maxHeight: Int
955
+ ): Int {
956
+ var inSampleSize = 1
957
+
958
+ if (reqHeight > maxHeight || reqWidth > maxWidth) {
959
+ val halfHeight = reqHeight / 2
960
+ val halfWidth = reqWidth / 2
961
+
962
+ while ((halfHeight / inSampleSize) >= maxHeight && (halfWidth / inSampleSize) >= maxWidth) {
963
+ inSampleSize *= 2
964
+ }
965
+ }
966
+
967
+ return inSampleSize
968
+ }
969
+
970
+ private fun resizeBitmap(bitmap: Bitmap, maxWidth: Int, maxHeight: Int): Bitmap {
971
+ val width = bitmap.width
972
+ val height = bitmap.height
973
+
974
+ val ratioBitmap = width.toFloat() / height.toFloat()
975
+ val ratioMax = maxWidth.toFloat() / maxHeight.toFloat()
976
+
977
+ var finalWidth = maxWidth
978
+ var finalHeight = maxHeight
979
+
980
+ if (ratioMax > ratioBitmap) {
981
+ finalWidth = (maxHeight * ratioBitmap).toInt()
982
+ } else {
983
+ finalHeight = (maxWidth / ratioBitmap).toInt()
984
+ }
985
+
986
+ return Bitmap.createScaledBitmap(bitmap, finalWidth, finalHeight, true)
987
+ }
988
+
989
+
990
+ // MARK: - Media gallery (사진 모아보기)
991
+
992
+ /** 열려 있는 미디어 갤러리 (galleryId → 화면). 메인 스레드에서만 접근 */
993
+ private val mediaGalleries = HashMap<String, MediaGalleryOverlay>()
994
+
995
+ private fun emitGalleryEvent(name: String, body: WritableMap) {
996
+ try {
997
+ reactApplicationContext
998
+ .getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
999
+ .emit(name, body)
1000
+ } catch (e: Exception) {
1001
+ android.util.Log.w("TurboMediaGallery", "emit $name failed", e)
1002
+ }
1003
+ }
1004
+
1005
+ /**
1006
+ * openMediaGallery(options) → { galleryId }
1007
+ * - dataSource: 등록된 네이티브 데이터 소스 이름 (MediaGalleryRegistry) — 페이지 요청이 JS 를 거치지 않음
1008
+ * - initialItems / nextCursor / hasMore: JS 공급 방식 (끝에 가까우면 onGalleryNeedPage → appendGalleryItems)
1009
+ * Activity 를 띄우지 않고 호스트 Activity 위에 View 로 얹습니다.
1010
+ */
1011
+ @ReactMethod
1012
+ fun openMediaGallery(options: ReadableMap, promise: Promise) {
1013
+ val activity = getCurrentActivity()
1014
+ if (activity == null) {
1015
+ promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
1016
+ return
1017
+ }
1018
+ val o: Map<String, Any?> = options.toHashMap()
1019
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
1020
+ try {
1021
+ val systemDark = (activity.resources.configuration.uiMode and
1022
+ android.content.res.Configuration.UI_MODE_NIGHT_MASK) == android.content.res.Configuration.UI_MODE_NIGHT_YES
1023
+ val config = MediaGalleryConfig(o, systemDark)
1024
+ val galleryId = config.galleryId
1025
+ if (mediaGalleries[galleryId]?.isShowing == true) {
1026
+ promise.reject("E_GALLERY_OPEN", "Media gallery '$galleryId' is already open")
1027
+ return@runOnUiThread
1028
+ }
1029
+ MediaGalleryRegistry.ensureDemo(activity.applicationContext)
1030
+ val dataSourceName = o["dataSource"] as? String
1031
+ val store: MediaGalleryStore
1032
+ if (!dataSourceName.isNullOrEmpty()) {
1033
+ val dataSource = MediaGalleryRegistry.source(dataSourceName)
1034
+ if (dataSource == null) {
1035
+ promise.reject("E_NO_DATA_SOURCE", "Media gallery data source '$dataSourceName' is not registered")
1036
+ return@runOnUiThread
1037
+ }
1038
+ @Suppress("UNCHECKED_CAST")
1039
+ val params = (o["dataSourceParams"] as? Map<String, Any?>) ?: emptyMap()
1040
+ store = MediaGalleryStore(galleryId, MediaGalleryStore.Source.Native(dataSource, params))
1041
+ } else {
1042
+ store = MediaGalleryStore(
1043
+ galleryId,
1044
+ MediaGalleryStore.Source.JavaScript,
1045
+ MediaGalleryItem.listFrom(o["initialItems"]),
1046
+ o["nextCursor"] as? String,
1047
+ o["hasMore"] as? Boolean ?: false
1048
+ )
1049
+ store.onNeedPage = { cursor ->
1050
+ emitGalleryEvent("onGalleryNeedPage", Arguments.createMap().apply {
1051
+ putString("galleryId", galleryId)
1052
+ if (cursor != null) putString("cursor", cursor) else putNull("cursor")
1053
+ })
1054
+ }
1055
+ }
1056
+ val overlay = MediaGalleryOverlay(activity, store, config)
1057
+ overlay.onClosed = {
1058
+ if (mediaGalleries[galleryId] === overlay) mediaGalleries.remove(galleryId)
1059
+ emitGalleryEvent("onGalleryClosed", Arguments.createMap().apply { putString("galleryId", galleryId) })
1060
+ }
1061
+ // 뷰어에서 삭제를 고르면: 모듈은 그 묶음을 그리드·뷰어에서 지우고, 실제 삭제는 앱이 하도록 이벤트만 보냄
1062
+ overlay.onItemDeleted = { item, option ->
1063
+ emitGalleryEvent("onGalleryItemDeleted", Arguments.createMap().apply {
1064
+ putString("galleryId", galleryId)
1065
+ putString("id", item.id)
1066
+ putString("groupId", item.groupId)
1067
+ if (option != null) putString("option", option) else putNull("option")
1068
+ })
1069
+ }
1070
+ // 상단 바 메뉴에서 그리드 크기를 바꾸면 (모듈이 기기에 저장, 앱에도 알림)
1071
+ overlay.onGridSizeChanged = { columns ->
1072
+ emitGalleryEvent("onGalleryGridSizeChanged", Arguments.createMap().apply {
1073
+ putString("galleryId", galleryId)
1074
+ putString("gridSize", MediaGalleryGridPreference.gridSizeString(columns))
1075
+ })
1076
+ }
1077
+ mediaGalleries[galleryId] = overlay
1078
+ overlay.show()
1079
+ promise.resolve(Arguments.createMap().apply { putString("galleryId", galleryId) })
1080
+ } catch (e: Exception) {
1081
+ promise.reject("E_MEDIA_GALLERY", "Failed to open media gallery: ${e.message}", e)
1082
+ }
1083
+ }
1084
+ }
1085
+
1086
+ @ReactMethod
1087
+ fun closeMediaGallery(galleryId: String, promise: Promise) {
1088
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
1089
+ mediaGalleries[galleryId]?.close(includeViewer = true)
1090
+ promise.resolve(null)
1091
+ }
1092
+ }
1093
+
1094
+ /** JS 공급 방식: { galleryId, items, nextCursor?, hasMore } */
1095
+ @ReactMethod
1096
+ fun appendGalleryItems(payload: ReadableMap, promise: Promise) {
1097
+ val p: Map<String, Any?> = payload.toHashMap()
1098
+ val galleryId = p["galleryId"] as? String ?: ""
1099
+ val page = MediaGalleryPage(
1100
+ MediaGalleryItem.listFrom(p["items"]),
1101
+ p["nextCursor"] as? String,
1102
+ p["hasMore"] as? Boolean ?: false
1103
+ )
1104
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
1105
+ mediaGalleries[galleryId]?.store?.appendExternal(page)
1106
+ promise.resolve(null)
1107
+ }
1108
+ }
1109
+
1110
+ /** 앱이 다른 경로로 지운 묶음 반영: { galleryId, groupIds } */
1111
+ @ReactMethod
1112
+ fun removeGalleryItems(payload: ReadableMap, promise: Promise) {
1113
+ val p: Map<String, Any?> = payload.toHashMap()
1114
+ val galleryId = p["galleryId"] as? String ?: ""
1115
+ val groupIds = (p["groupIds"] as? List<*> ?: emptyList<Any?>()).mapNotNull { v ->
1116
+ when (v) {
1117
+ is String -> v
1118
+ is Number -> v.toDouble().let { d -> if (d % 1.0 == 0.0) d.toLong().toString() else d.toString() }
1119
+ else -> null
1120
+ }
1121
+ }.toSet()
1122
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
1123
+ mediaGalleries[galleryId]?.store?.removeGroups(groupIds)
1124
+ promise.resolve(null)
1125
+ }
1126
+ }
1127
+
1128
+ override fun invalidate() {
1129
+ super.invalidate()
1130
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
1131
+ val open = mediaGalleries.values.toList()
1132
+ mediaGalleries.clear()
1133
+ open.forEach { it.dismissImmediately() }
1134
+ }
1135
+ reactApplicationContext.removeActivityEventListener(this)
1136
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
1137
+ .unregisterReceiver(viewerOpenedReceiver)
1138
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
1139
+ .unregisterReceiver(imageDeletedReceiver)
1140
+ }
1141
+
1142
+ }