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,1159 @@
1
+ package com.rnattachmentkit
2
+
3
+
4
+ import android.app.Activity
5
+ import android.content.Intent
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
8
+ import android.net.Uri
9
+ import com.facebook.react.bridge.*
10
+ import com.facebook.react.module.annotations.ReactModule
11
+ import android.Manifest
12
+ import android.content.pm.PackageManager
13
+ import android.os.Build
14
+ import androidx.core.content.ContextCompat
15
+ import androidx.fragment.app.FragmentActivity
16
+ import com.facebook.react.modules.core.PermissionAwareActivity
17
+ import com.facebook.react.modules.core.PermissionListener
18
+ import kotlinx.coroutines.CoroutineScope
19
+ import kotlinx.coroutines.Dispatchers
20
+ import kotlinx.coroutines.launch
21
+ import kotlinx.coroutines.withContext
22
+ import java.io.*
23
+
24
+ @ReactModule(name = RNAttachmentKitModule.NAME)
25
+ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
26
+ NativeAttachmentKitSpec(reactContext), ActivityEventListener {
27
+
28
+ companion object {
29
+ const val NAME = "RNAttachmentKit"
30
+ private const val E_PICKER_CANCELLED = "E_PICKER_CANCELLED"
31
+ private const val E_NO_ACTIVITY = "E_NO_ACTIVITY"
32
+ private const val E_FAILED_TO_PICK = "E_FAILED_TO_PICK"
33
+ private const val REQUEST_CODE_IMAGE_PICKER = 3001
34
+ private const val REQUEST_CODE_IMAGE_EDITOR = 3002
35
+ }
36
+
37
+ private var pickImagesPromise: Promise? = null
38
+ private var editImagePromise: Promise? = null
39
+ private val moduleScope = CoroutineScope(Dispatchers.Main)
40
+ private var maxWidth: Int = 1024
41
+ private var maxHeight: Int = 1024
42
+ private var outputFormat: String = "webp" // Default to webp
43
+
44
+ // newarch에는 이 맵과 getDefaultAnimationConfig() 구현이 통째로 빠져 있었습니다(oldarch에는 있었음).
45
+ // NativeAttachmentKitSpec(codegen 생성 추상 클래스)이 이 메서드를 abstract로 요구하기 때문에,
46
+ // 누락된 상태로는 아예 컴파일이 안 됩니다(newarch는 AAR이 아니라 소스 형태로 배포되어 소비 앱이
47
+ // 직접 컴파일하므로, 이 파일을 실제로 빌드해보기 전까지는 드러나지 않았던 것으로 보입니다).
48
+ private val defaultAnimationConfig = mapOf(
49
+ "galleryOpen" to 300,
50
+ "galleryClose" to 250,
51
+ "editorOpen" to 250,
52
+ "editorClose" to 200,
53
+ "viewerOpen" to 250,
54
+ "viewerClose" to 200
55
+ )
56
+
57
+ // newarch에는 이 리시버가 누락되어 있었습니다(oldarch에는 있었음). 그 결과 네이티브가
58
+ // VIEWER_WILL_CLOSE 로컬 브로드캐스트를 보내도 JS의 "onViewerWillClose" 이벤트가 한 번도
59
+ // emit되지 않아서, JS(index.ts)의 리스너 정리 로직(subscriptionPageSelected.remove() 등)이
60
+ // 실행되지 않고 계속 leak됐습니다. 그 결과 이전에 열었던 이미지의 onPageSelected 구독이 살아있는
61
+ // 채로 다음 이미지를 열면, 두 구독이 동시에 반응해서 "이전 이미지의 위치 정보"로
62
+ // updateSourceRect()가 잘못 호출되어 닫기 애니메이션이 엉뚱한(이전) 위치로 돌아가는
63
+ // 버그의 원인이었습니다.
64
+ private val viewerWillCloseReceiver = object : android.content.BroadcastReceiver() {
65
+ override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
66
+ if (intent?.action == "com.rnattachmentkit.VIEWER_WILL_CLOSE") {
67
+ reactApplicationContext
68
+ .getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
69
+ .emit("onViewerWillClose", Arguments.createMap())
70
+ }
71
+ }
72
+ }
73
+
74
+ private val pageChangeReceiver = object : android.content.BroadcastReceiver() {
75
+ override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
76
+ if (intent?.action == "com.rnattachmentkit.PAGE_CHANGED") {
77
+ val index = intent.getIntExtra("index", 0)
78
+ val params = Arguments.createMap()
79
+ params.putInt("index", index)
80
+ reactApplicationContext
81
+ .getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
82
+ .emit("onPageSelected", params)
83
+ }
84
+ }
85
+ }
86
+
87
+ // 이미지 뷰어 상단바의 삭제를 사용자가 확정했을 때(ImageViewerOverlayView.handleDeleteTapped).
88
+ // iOS의 "onImageDeleted" 이벤트와 동일한 body(index, url)로 JS에 알립니다 - 실제 메시지 삭제는 RN이 담당.
89
+ private val imageDeletedReceiver = object : android.content.BroadcastReceiver() {
90
+ override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
91
+ if (intent?.action == "com.rnattachmentkit.IMAGE_DELETED") {
92
+ val params = Arguments.createMap()
93
+ params.putInt("index", intent.getIntExtra("index", 0))
94
+ params.putString("url", intent.getStringExtra("url") ?: "")
95
+ // patch405: 사용자가 고른 삭제 방식(deleteOptions 의 key). 선택지가 없었으면 생략.
96
+ intent.getStringExtra("option")?.let { params.putString("option", it) }
97
+ reactApplicationContext
98
+ .getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
99
+ .emit("onImageDeleted", params)
100
+ }
101
+ }
102
+ }
103
+
104
+ // 열기 애니메이션이 끝난 시점을 JS에 알리는 이벤트 (배경 스크롤 보정 타이밍용).
105
+ private val viewerOpenedReceiver = object : android.content.BroadcastReceiver() {
106
+ override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
107
+ if (intent?.action == "com.rnattachmentkit.VIEWER_OPENED") {
108
+ reactApplicationContext
109
+ .getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
110
+ .emit("onViewerOpened", Arguments.createMap())
111
+ }
112
+ }
113
+ }
114
+
115
+ init {
116
+ reactContext.addActivityEventListener(this)
117
+ }
118
+
119
+ override fun getDefaultAnimationConfig(promise: Promise) {
120
+ val config = Arguments.createMap().apply {
121
+ putInt("galleryOpen", defaultAnimationConfig["galleryOpen"] ?: 300)
122
+ putInt("galleryClose", defaultAnimationConfig["galleryClose"] ?: 250)
123
+ putInt("editorOpen", defaultAnimationConfig["editorOpen"] ?: 250)
124
+ putInt("editorClose", defaultAnimationConfig["editorClose"] ?: 200)
125
+ putInt("viewerOpen", defaultAnimationConfig["viewerOpen"] ?: 250)
126
+ putInt("viewerClose", defaultAnimationConfig["viewerClose"] ?: 200)
127
+ }
128
+ promise.resolve(config)
129
+ }
130
+
131
+ override fun initialize() {
132
+ super.initialize()
133
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
134
+ .registerReceiver(pageChangeReceiver, android.content.IntentFilter("com.rnattachmentkit.PAGE_CHANGED"))
135
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
136
+ .registerReceiver(viewerOpenedReceiver, android.content.IntentFilter("com.rnattachmentkit.VIEWER_OPENED"))
137
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
138
+ .registerReceiver(viewerWillCloseReceiver, android.content.IntentFilter("com.rnattachmentkit.VIEWER_WILL_CLOSE"))
139
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
140
+ .registerReceiver(imageDeletedReceiver, android.content.IntentFilter("com.rnattachmentkit.IMAGE_DELETED"))
141
+ }
142
+
143
+ override fun updateSourceRect(options: ReadableMap, promise: Promise) {
144
+ var startX = -1f
145
+ var startY = -1f
146
+ var startWidth = -1f
147
+ var startHeight = -1f
148
+
149
+ if (options.hasKey("x")) {
150
+ startX = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("x").toFloat())
151
+ }
152
+ if (options.hasKey("y")) {
153
+ startY = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("y").toFloat())
154
+ }
155
+ if (options.hasKey("width")) {
156
+ startWidth = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("width").toFloat())
157
+ }
158
+ if (options.hasKey("height")) {
159
+ startHeight = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("height").toFloat())
160
+ }
161
+
162
+ // 브로드캐스트가 유실되는 타이밍(Fragment의 onViewCreated 전) 대비, sticky 값도 함께 저장.
163
+ // (useDialogViewer/useOverlayViewer 중 실제 사용 중인 쪽만 값을 읽으므로 둘 다 갱신해도 무해함)
164
+ ImageViewerDialogFragment.updatePendingCoordinates(startX, startY, startWidth, startHeight)
165
+ ImageViewerOverlayView.updatePendingCoordinates(startX, startY, startWidth, startHeight)
166
+
167
+ val intent = android.content.Intent("com.rnattachmentkit.UPDATE_COORDINATES")
168
+ intent.putExtra("startX", startX)
169
+ intent.putExtra("startY", startY)
170
+ intent.putExtra("startWidth", startWidth)
171
+ intent.putExtra("startHeight", startHeight)
172
+ // 그동안 sourceBorderCorners(어느 모서리를 둥글릴지)가 여기서 전달되지 않아서, 그룹 이미지에서
173
+ // 스와이프로 다른 셀(예: 오른쪽 위 모서리 이미지)로 이동해도 마스크의 라운드 코너 패턴이
174
+ // 처음 열었던 이미지의 것으로 고정된 채 안 바뀌는 문제가 있었습니다. JS가 onPageSelected에서
175
+ // 매번 새로 계산해서 보내주는 sourceBorderCorners를 브로드캐스트에 실어 보내도록 수정합니다.
176
+ if (options.hasKey("sourceBorderCorners")) {
177
+ val cornersArray = options.getArray("sourceBorderCorners")
178
+ if (cornersArray != null) {
179
+ val corners = ArrayList<String>()
180
+ for (i in 0 until cornersArray.size()) {
181
+ cornersArray.getString(i)?.let { corners.add(it) }
182
+ }
183
+ intent.putStringArrayListExtra("sourceBorderCorners", corners)
184
+ }
185
+ }
186
+
187
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
188
+ .sendBroadcast(intent)
189
+
190
+ promise.resolve(null)
191
+ }
192
+
193
+ override fun getName(): String = NAME
194
+
195
+ override fun init(licenseKey: String, promise: Promise) {
196
+ val result = LicenseManager.initialize(reactApplicationContext, licenseKey)
197
+ promise.resolve(result)
198
+ }
199
+
200
+ override fun openEditor(options: ReadableMap, promise: Promise) {
201
+ val activity = getCurrentActivity()
202
+ if (activity == null) {
203
+ promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
204
+ return
205
+ }
206
+
207
+ val uriStr = if (options.hasKey("uri")) options.getString("uri") else null
208
+ if (uriStr == null) {
209
+ promise.reject(E_FAILED_TO_PICK, "uri is required")
210
+ return
211
+ }
212
+
213
+ val themeColor = if (options.hasKey("themeColor")) options.getString("themeColor") else null
214
+
215
+ if (options.hasKey("maxWidth")) maxWidth = options.getInt("maxWidth")
216
+ if (options.hasKey("maxHeight")) maxHeight = options.getInt("maxHeight")
217
+
218
+ editImagePromise = promise
219
+
220
+ try {
221
+ val intent = ImageEditorActivity.createIntent(
222
+ activity = activity,
223
+ uris = listOf(Uri.parse(uriStr)),
224
+ startIndex = 0,
225
+ themeColor = themeColor,
226
+ singlePhotoMode = true
227
+ )
228
+ android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
229
+ activity.startActivityForResult(intent, REQUEST_CODE_IMAGE_EDITOR)
230
+ }, 50)
231
+ } catch (e: Exception) {
232
+ promise.reject(E_FAILED_TO_PICK, "Failed to open image editor: ${e.message}", e)
233
+ editImagePromise = null
234
+ }
235
+ }
236
+
237
+ @ReactMethod
238
+ override fun openGallery(options: ReadableMap, promise: Promise) {
239
+ val activity = getCurrentActivity()
240
+
241
+ if (activity == null) {
242
+ promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
243
+ return
244
+ }
245
+
246
+ // Parse options
247
+ val autoCloseOnSelect = if (options.hasKey("autoCloseOnSelect")) {
248
+ options.getBoolean("autoCloseOnSelect")
249
+ } else false
250
+
251
+ // 선택 방식 (selectionMode: 'single' | 'multi', 생략 시 기존 규칙)
252
+ // - 'multi' : maxSelection 이 1 이어도 멀티와 똑같이 (autoCloseOnSelect 무시, 1 미만이면 1장)
253
+ // - 'single': 기존 단일 동작 (maxSelection 을 1로, autoCloseOnSelect 면 0)
254
+ // - 생략 : 기존 그대로 (autoCloseOnSelect → 0, 아니면 maxSelection, 기본 1)
255
+ val selectionMode = if (options.hasKey("selectionMode")) options.getString("selectionMode")?.lowercase() else null
256
+ val forceMultiMode = selectionMode == "multi"
257
+ val maxSelection = if (forceMultiMode) {
258
+ (if (options.hasKey("maxSelection")) options.getInt("maxSelection") else 1).coerceAtLeast(1)
259
+ } else if (autoCloseOnSelect) {
260
+ 0
261
+ } else if (selectionMode == "single") {
262
+ 1
263
+ } else if (options.hasKey("maxSelection")) {
264
+ options.getInt("maxSelection")
265
+ } else {
266
+ 1
267
+ }
268
+
269
+ val languageCode = if (options.hasKey("languageCode")) {
270
+ options.getString("languageCode") ?: "en"
271
+ } else {
272
+ "en"
273
+ }
274
+ // 편집기/크롭 등 다른 화면들은 TurboImagePickerConfig.languageCode(전역)로 로케일을 잡는데,
275
+ // 그동안 JS가 넘긴 languageCode를 여기에 반영하는 코드가 없어서 항상 기본값 "en"이었습니다.
276
+ TurboImagePickerConfig.languageCode = languageCode
277
+
278
+ val themeColor = if (options.hasKey("themeColor")) {
279
+ options.getString("themeColor")
280
+ } else {
281
+ null
282
+ }
283
+
284
+ maxWidth = if (options.hasKey("maxWidth")) {
285
+ val value = options.getInt("maxWidth")
286
+ if (value > 0) value else 1024
287
+ } else {
288
+ 1024
289
+ }
290
+
291
+ maxHeight = if (options.hasKey("maxHeight")) {
292
+ val value = options.getInt("maxHeight")
293
+ if (value > 0) value else 1024
294
+ } else {
295
+ 1024
296
+ }
297
+
298
+ outputFormat = if (options.hasKey("outputFormat")) {
299
+ options.getString("outputFormat")?.lowercase() ?: "webp"
300
+ } else if (options.hasKey("format")) {
301
+ options.getString("format")?.lowercase() ?: "webp"
302
+ } else {
303
+ "webp"
304
+ }
305
+
306
+ val enableEditor = if (options.hasKey("enableEditor")) {
307
+ options.getBoolean("enableEditor")
308
+ } else {
309
+ false
310
+ }
311
+
312
+ val profileMode = if (options.hasKey("profileMode")) {
313
+ options.getBoolean("profileMode")
314
+ } else {
315
+ false
316
+ }
317
+
318
+ val fragmentActivity = activity as? FragmentActivity
319
+ if (fragmentActivity == null) {
320
+ promise.reject(E_NO_ACTIVITY, "Activity is not a FragmentActivity")
321
+ return
322
+ }
323
+
324
+ pickImagesPromise = promise
325
+
326
+ val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
327
+ Manifest.permission.READ_MEDIA_IMAGES
328
+ } else {
329
+ Manifest.permission.READ_EXTERNAL_STORAGE
330
+ }
331
+
332
+ val showSheet = {
333
+ fragmentActivity.runOnUiThread {
334
+ try {
335
+ val bottomSheet = ImagePickerBottomSheet.newInstance(
336
+ maxSelection, languageCode, enableEditor, profileMode, maxWidth, maxHeight, themeColor,
337
+ forceMultiMode = forceMultiMode
338
+ )
339
+ bottomSheet.setOnImagesSelectedListener { uris ->
340
+ val result = WritableNativeArray()
341
+ for (uri in uris) {
342
+ result.pushString(uri.toString())
343
+ }
344
+ handleSelectedImages(result, pickImagesPromise)
345
+ pickImagesPromise = null
346
+ }
347
+ bottomSheet.setOnCancelledListener {
348
+ pickImagesPromise?.reject(E_PICKER_CANCELLED, "User cancelled image picker")
349
+ pickImagesPromise = null
350
+ }
351
+
352
+ val existingFragment = fragmentActivity.supportFragmentManager.findFragmentByTag("ImagePickerBottomSheet")
353
+ existingFragment?.let {
354
+ fragmentActivity.supportFragmentManager.beginTransaction().remove(it).commitAllowingStateLoss()
355
+ }
356
+
357
+ fragmentActivity.supportFragmentManager.beginTransaction()
358
+ .add(bottomSheet, "ImagePickerBottomSheet")
359
+ .commitAllowingStateLoss()
360
+
361
+ } catch (e: Exception) {
362
+ pickImagesPromise?.reject(E_FAILED_TO_PICK, "Failed to open image picker: ${e.message}", e)
363
+ pickImagesPromise = null
364
+ }
365
+ }
366
+ }
367
+
368
+ if (ContextCompat.checkSelfPermission(fragmentActivity, permission) == PackageManager.PERMISSION_GRANTED) {
369
+ showSheet()
370
+ } else {
371
+ val permissionAwareActivity = fragmentActivity as? PermissionAwareActivity
372
+ if (permissionAwareActivity == null) {
373
+ promise.reject(E_NO_ACTIVITY, "Activity is not PermissionAwareActivity")
374
+ pickImagesPromise = null
375
+ return
376
+ }
377
+
378
+ val permissionsToRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && permission == Manifest.permission.READ_MEDIA_IMAGES) {
379
+ if (ContextCompat.checkSelfPermission(fragmentActivity, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) == PackageManager.PERMISSION_GRANTED) {
380
+ showSheet()
381
+ return
382
+ }
383
+ arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED)
384
+ } else {
385
+ arrayOf(permission)
386
+ }
387
+
388
+ permissionAwareActivity.requestPermissions(permissionsToRequest, 100, object : PermissionListener {
389
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray): Boolean {
390
+ if (requestCode == 100 && grantResults != null) {
391
+ val isGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
392
+ grantResults.isNotEmpty() && (
393
+ grantResults[0] == PackageManager.PERMISSION_GRANTED ||
394
+ (grantResults.size > 1 && grantResults[1] == PackageManager.PERMISSION_GRANTED)
395
+ )
396
+ } else {
397
+ grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
398
+ }
399
+
400
+ if (isGranted) {
401
+ showSheet()
402
+ } else {
403
+ pickImagesPromise?.reject("E_PERMISSION_DENIED", "Permission denied")
404
+ pickImagesPromise = null
405
+ }
406
+ }
407
+ return true
408
+ }
409
+ })
410
+ }
411
+ }
412
+
413
+ override fun openViewer(options: ReadableMap, promise: Promise) {
414
+ val activity = getCurrentActivity()
415
+ if (activity == null) {
416
+ promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
417
+ return
418
+ }
419
+
420
+ if (!options.hasKey("images")) {
421
+ promise.reject(E_FAILED_TO_PICK, "images array is required")
422
+ return
423
+ }
424
+
425
+ val imagesArray = options.getArray("images")
426
+ if (imagesArray == null || imagesArray.size() == 0) {
427
+ promise.reject(E_FAILED_TO_PICK, "images array cannot be empty")
428
+ return
429
+ }
430
+
431
+ val images = ArrayList<String>()
432
+ for (i in 0 until imagesArray.size()) {
433
+ val str = imagesArray.getString(i)
434
+ // ⚠️ null을 그냥 건너뛰면(add를 안 하면) 그 뒤 모든 이미지의 인덱스가 하나씩
435
+ // 앞으로 당겨져서, initialIndex/placeholderImages와 어긋나는 "인덱스 밀림" 버그가
436
+ // 됩니다. 아래 placeholderImages 루프처럼 빈 문자열로라도 자리를 채워 인덱스를
437
+ // 보존합니다.
438
+ images.add(str ?: "")
439
+ }
440
+
441
+ // cacheFiles: images 와 같은 순서의 앱 캐시 파일 경로 (null → 빈 문자열). 뷰어 전체(모든 방식)에 적용.
442
+ val cacheFiles = ArrayList<String>()
443
+ if (options.hasKey("cacheFiles") && !options.isNull("cacheFiles")) {
444
+ val arr = options.getArray("cacheFiles")
445
+ if (arr != null) {
446
+ for (i in 0 until arr.size()) {
447
+ cacheFiles.add(if (arr.isNull(i)) "" else (arr.getString(i) ?: ""))
448
+ }
449
+ }
450
+ }
451
+ ViewerCacheFiles.register(images, if (cacheFiles.isEmpty()) null else cacheFiles)
452
+
453
+ val initialIndex = if (options.hasKey("initialIndex")) options.getInt("initialIndex") else 0
454
+ val themeColor = if (options.hasKey("themeColor")) options.getString("themeColor") else "#FF6B35"
455
+ val title = if (options.hasKey("title")) options.getString("title") else null
456
+ val animationType = if (options.hasKey("animationType")) options.getString("animationType") else null
457
+ // ⚠️ 뷰어가 항상 영어로만 나오던 원인: JS는 init()에서 설정한 기본 언어를 openViewer 옵션의
458
+ // languageCode로 매번 같이 넘기는데, 안드로이드 쪽에서는 그 값을 한 번도 읽지 않았고
459
+ // ImageViewerOverlayView는 TurboImagePickerConfig.attachBaseContext()(전역 languageCode,
460
+ // 기본값 "en")로 로케일을 강제하고 있었습니다. 여기서 반영해야 뷰어의 모든 문구가
461
+ // 설정된 언어를 따릅니다.
462
+ val viewerLanguageCode = if (options.hasKey("languageCode")) (options.getString("languageCode") ?: "en") else "en"
463
+ TurboImagePickerConfig.languageCode = viewerLanguageCode
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
+ if (startX != -1f) {
657
+ putExtra("startX", startX)
658
+ putExtra("startY", startY)
659
+ putExtra("startWidth", startWidth)
660
+ putExtra("startHeight", startHeight)
661
+ }
662
+
663
+ if (options.hasKey("sourceBorderRadius")) {
664
+ putExtra("sourceBorderRadius", com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(options.getDouble("sourceBorderRadius").toFloat()))
665
+ }
666
+ if (options.hasKey("sourceBorderCorners")) {
667
+ val cornersArray = options.getArray("sourceBorderCorners")
668
+ if (cornersArray != null) {
669
+ val corners = ArrayList<String>()
670
+ for (i in 0 until cornersArray.size()) {
671
+ cornersArray.getString(i)?.let { corners.add(it) }
672
+ }
673
+ putStringArrayListExtra("sourceBorderCorners", corners)
674
+ }
675
+ }
676
+ if (options.hasKey("sourceBackgroundColor")) {
677
+ putExtra("sourceBackgroundColor", options.getString("sourceBackgroundColor"))
678
+ }
679
+ if (options.hasKey("placeholderImages")) {
680
+ val placeholderArray = options.getArray("placeholderImages")
681
+ if (placeholderArray != null) {
682
+ val placeholders = ArrayList<String>()
683
+ for (i in 0 until placeholderArray.size()) {
684
+ placeholders.add(placeholderArray.getString(i) ?: "")
685
+ }
686
+ putStringArrayListExtra("placeholderImages", placeholders)
687
+ }
688
+ }
689
+ }
690
+ activity.startActivity(intent)
691
+ promise.resolve(null)
692
+ } catch (e: Exception) {
693
+ promise.reject(E_FAILED_TO_PICK, "Failed to open image viewer: ${e.message}", e)
694
+ }
695
+ }
696
+
697
+ override fun closeGallery(promise: Promise) {
698
+ val activity = getCurrentActivity()
699
+ if (activity == null) {
700
+ promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
701
+ return
702
+ }
703
+ activity.finishActivity(REQUEST_CODE_IMAGE_PICKER)
704
+ promise.resolve(true)
705
+ }
706
+
707
+ override fun addListener(eventName: String) {
708
+ // Keep: Required for RN built-in Event Emitter Calls.
709
+ }
710
+
711
+ override fun removeListeners(count: Double) {
712
+ // Keep: Required for RN built-in Event Emitter Calls.
713
+ }
714
+
715
+ override fun onActivityResult(
716
+ activity: Activity,
717
+ requestCode: Int,
718
+ resultCode: Int,
719
+ data: Intent?
720
+ ) {
721
+ if (requestCode == REQUEST_CODE_IMAGE_EDITOR) {
722
+ val promise = editImagePromise ?: return
723
+ editImagePromise = null
724
+
725
+ if (resultCode != Activity.RESULT_OK || data == null) {
726
+ promise.reject(E_PICKER_CANCELLED, "User cancelled image editor")
727
+ return
728
+ }
729
+
730
+ val uriStrings = data.getStringArrayListExtra(ImageEditorActivity.EXTRA_RESULT_URIS)
731
+ if (uriStrings == null || uriStrings.isEmpty()) {
732
+ promise.reject(E_PICKER_CANCELLED, "User cancelled image editor")
733
+ return
734
+ }
735
+
736
+ val uris = uriStrings.mapNotNull { uriString ->
737
+ try { Uri.parse(uriString) } catch (e: Exception) { null }
738
+ }
739
+
740
+ if (uris.isEmpty()) {
741
+ promise.reject(E_FAILED_TO_PICK, "Failed to parse image URIs")
742
+ return
743
+ }
744
+
745
+ moduleScope.launch {
746
+ try {
747
+ val results = processImages(uris)
748
+ if (results.isNotEmpty()) {
749
+ promise.resolve(results[0])
750
+ } else {
751
+ promise.reject(E_FAILED_TO_PICK, "Failed to process image")
752
+ }
753
+ } catch (e: Exception) {
754
+ promise.reject(E_FAILED_TO_PICK, "Failed to process image: ${e.message}", e)
755
+ }
756
+ }
757
+ return
758
+ }
759
+
760
+
761
+ }
762
+
763
+ override fun onNewIntent(intent: Intent) {
764
+ // Not needed for this module
765
+ }
766
+
767
+ private fun handleSelectedImages(uriStrings: ReadableArray, promise: Promise?) {
768
+ val actualPromise = promise ?: return
769
+
770
+ // Convert string URIs to Uri objects
771
+ val uris = mutableListOf<Uri>()
772
+ for (i in 0 until uriStrings.size()) {
773
+ val uriString = uriStrings.getString(i)
774
+ if (uriString != null) {
775
+ try {
776
+ uris.add(Uri.parse(uriString))
777
+ } catch (e: Exception) {
778
+ // Ignore
779
+ }
780
+ }
781
+ }
782
+
783
+ if (uris.isEmpty()) {
784
+ actualPromise.reject(E_FAILED_TO_PICK, "Failed to parse image URIs")
785
+ return
786
+ }
787
+
788
+ // Process images asynchronously
789
+ val eventMap = com.facebook.react.bridge.WritableNativeMap()
790
+ eventMap.putInt("selectedCount", uris.size)
791
+ eventMap.putInt("maxSelection", 10)
792
+ try {
793
+ reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
794
+ .emit("onSelectionChange", eventMap)
795
+ } catch (e: Exception) {
796
+ // Ignore
797
+ }
798
+
799
+ moduleScope.launch {
800
+ try {
801
+ val results = processImages(uris)
802
+ val array = com.facebook.react.bridge.Arguments.createArray()
803
+ results.forEach { array.pushMap(it) }
804
+ actualPromise.resolve(array)
805
+ } catch (e: Exception) {
806
+ actualPromise.reject(E_FAILED_TO_PICK, "Failed to process images: ${e.message}", e)
807
+ }
808
+ }
809
+ }
810
+
811
+ private suspend fun processImages(uris: List<Uri>): List<com.facebook.react.bridge.WritableMap> = withContext(Dispatchers.IO) {
812
+ val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
813
+ val contentResolver = reactApplicationContext.contentResolver
814
+ val totalCount = uris.size
815
+ var currentIndex = 0
816
+
817
+ for (uri in uris) {
818
+ try {
819
+ // 1. Get EXIF Orientation
820
+ var orientation = android.media.ExifInterface.ORIENTATION_NORMAL
821
+ try {
822
+ contentResolver.openInputStream(uri)?.use { exifInputStream ->
823
+ val exif = android.media.ExifInterface(exifInputStream)
824
+ orientation = exif.getAttributeInt(android.media.ExifInterface.TAG_ORIENTATION, android.media.ExifInterface.ORIENTATION_NORMAL)
825
+ }
826
+ } catch (e: Exception) {}
827
+
828
+ contentResolver.openInputStream(uri)?.use { inputStream ->
829
+ // Get original image dimensions
830
+ val originalOptions = BitmapFactory.Options().apply {
831
+ inJustDecodeBounds = true
832
+ }
833
+ BitmapFactory.decodeStream(inputStream, null, originalOptions)
834
+
835
+ var originalWidth = originalOptions.outWidth
836
+ var originalHeight = originalOptions.outHeight
837
+
838
+ // Swap width and height if rotated by 90 or 270 degrees
839
+ if (orientation == android.media.ExifInterface.ORIENTATION_ROTATE_90 || orientation == android.media.ExifInterface.ORIENTATION_ROTATE_270) {
840
+ val temp = originalWidth
841
+ originalWidth = originalHeight
842
+ originalHeight = temp
843
+ }
844
+
845
+ // Load and resize image
846
+ contentResolver.openInputStream(uri)?.use { resizedInputStream ->
847
+ // Initial decode for sizing
848
+ val decodeOptions = BitmapFactory.Options().apply {
849
+ inSampleSize = calculateInSampleSize(originalWidth, originalHeight, maxWidth, maxHeight)
850
+ }
851
+ var bitmap = BitmapFactory.decodeStream(resizedInputStream, null, decodeOptions)
852
+
853
+ if (bitmap != null) {
854
+ // Apply EXIF Rotation
855
+ val matrix = android.graphics.Matrix()
856
+ when (orientation) {
857
+ android.media.ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
858
+ android.media.ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
859
+ android.media.ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
860
+ android.media.ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1f, 1f)
861
+ android.media.ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1f, -1f)
862
+ }
863
+
864
+ if (!matrix.isIdentity) {
865
+ val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
866
+ if (rotatedBitmap != bitmap) {
867
+ bitmap.recycle()
868
+ bitmap = rotatedBitmap
869
+ }
870
+ }
871
+
872
+ // Precise resize if needed
873
+ val finalBitmap = if (bitmap.width > maxWidth || bitmap.height > maxHeight) {
874
+ resizeBitmap(bitmap, maxWidth, maxHeight)
875
+ } else {
876
+ bitmap
877
+ }
878
+
879
+ val processedBitmap = ImageProcessor.applyWatermarkIfNeeded(reactApplicationContext, finalBitmap)
880
+
881
+ // Determine format and extension
882
+ val compressFormat: Bitmap.CompressFormat
883
+ val extension: String
884
+ val mimeType: String
885
+
886
+ when (outputFormat) {
887
+ "png" -> {
888
+ compressFormat = Bitmap.CompressFormat.PNG
889
+ extension = "png"
890
+ mimeType = "image/png"
891
+ }
892
+ "jpg", "jpeg" -> {
893
+ compressFormat = Bitmap.CompressFormat.JPEG
894
+ extension = "jpg"
895
+ mimeType = "image/jpeg"
896
+ }
897
+ else -> { // webp
898
+ compressFormat = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
899
+ Bitmap.CompressFormat.WEBP_LOSSY
900
+ } else {
901
+ @Suppress("DEPRECATION")
902
+ Bitmap.CompressFormat.WEBP
903
+ }
904
+ extension = "webp"
905
+ mimeType = "image/webp"
906
+ }
907
+ }
908
+
909
+ // Save to temporary file
910
+ val tempFile = File.createTempFile("turbo_edited_${System.currentTimeMillis()}_", ".$extension", reactApplicationContext.cacheDir)
911
+ FileOutputStream(tempFile).use { out ->
912
+ processedBitmap.compress(compressFormat, 85, out)
913
+ }
914
+
915
+ val finalWidth = processedBitmap.width
916
+ val finalHeight = processedBitmap.height
917
+
918
+ // Cleanup
919
+ if (bitmap != processedBitmap) {
920
+ bitmap.recycle()
921
+ }
922
+ if (finalBitmap != processedBitmap && finalBitmap != bitmap) {
923
+ finalBitmap.recycle()
924
+ }
925
+ processedBitmap.recycle()
926
+
927
+ val createImageInfo = {
928
+ WritableNativeMap().apply {
929
+ putString("uri", Uri.fromFile(tempFile).toString())
930
+ putInt("width", finalWidth)
931
+ putInt("height", finalHeight)
932
+ putString("type", mimeType)
933
+ putString("fileName", tempFile.name)
934
+ putString("fileExtension", extension)
935
+ putDouble("fileSize", tempFile.length().toDouble())
936
+ putString("originalUri", uri.toString())
937
+ putInt("originalWidth", originalWidth)
938
+ putInt("originalHeight", originalHeight)
939
+ }
940
+ }
941
+ val imageInfo = createImageInfo()
942
+ results.add(imageInfo)
943
+
944
+ try {
945
+ val eventMap = WritableNativeMap().apply {
946
+ putInt("total", totalCount)
947
+ putInt("index", currentIndex)
948
+ putMap("image", createImageInfo())
949
+ }
950
+ reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
951
+ .emit("onImageProcessed", eventMap)
952
+ } catch (e: Exception) { }
953
+ }
954
+ }
955
+ }
956
+ currentIndex++
957
+ } catch (e: Exception) {
958
+ currentIndex++
959
+ // Skip failed images
960
+ continue
961
+ }
962
+ }
963
+
964
+ results
965
+ }
966
+
967
+ private fun calculateInSampleSize(
968
+ reqWidth: Int,
969
+ reqHeight: Int,
970
+ maxWidth: Int,
971
+ maxHeight: Int
972
+ ): Int {
973
+ var inSampleSize = 1
974
+
975
+ if (reqHeight > maxHeight || reqWidth > maxWidth) {
976
+ val halfHeight = reqHeight / 2
977
+ val halfWidth = reqWidth / 2
978
+
979
+ while ((halfHeight / inSampleSize) >= maxHeight && (halfWidth / inSampleSize) >= maxWidth) {
980
+ inSampleSize *= 2
981
+ }
982
+ }
983
+
984
+ return inSampleSize
985
+ }
986
+
987
+ private fun resizeBitmap(bitmap: Bitmap, maxWidth: Int, maxHeight: Int): Bitmap {
988
+ val width = bitmap.width
989
+ val height = bitmap.height
990
+
991
+ val ratioBitmap = width.toFloat() / height.toFloat()
992
+ val ratioMax = maxWidth.toFloat() / maxHeight.toFloat()
993
+
994
+ var finalWidth = maxWidth
995
+ var finalHeight = maxHeight
996
+
997
+ if (ratioMax > ratioBitmap) {
998
+ finalWidth = (maxHeight * ratioBitmap).toInt()
999
+ } else {
1000
+ finalHeight = (maxWidth / ratioBitmap).toInt()
1001
+ }
1002
+
1003
+ return Bitmap.createScaledBitmap(bitmap, finalWidth, finalHeight, true)
1004
+ }
1005
+
1006
+
1007
+ // MARK: - Media gallery (사진 모아보기)
1008
+
1009
+ /** 열려 있는 미디어 갤러리 (galleryId → 화면). 메인 스레드에서만 접근 */
1010
+ private val mediaGalleries = HashMap<String, MediaGalleryOverlay>()
1011
+
1012
+ private fun emitGalleryEvent(name: String, body: WritableMap) {
1013
+ try {
1014
+ reactApplicationContext
1015
+ .getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
1016
+ .emit(name, body)
1017
+ } catch (e: Exception) {
1018
+ android.util.Log.w("TurboMediaGallery", "emit $name failed", e)
1019
+ }
1020
+ }
1021
+
1022
+ /**
1023
+ * openMediaGallery(options) → { galleryId }
1024
+ * - dataSource: 등록된 네이티브 데이터 소스 이름 (MediaGalleryRegistry) — 페이지 요청이 JS 를 거치지 않음
1025
+ * - initialItems / nextCursor / hasMore: JS 공급 방식 (끝에 가까우면 onGalleryNeedPage → appendGalleryItems)
1026
+ * Activity 를 띄우지 않고 호스트 Activity 위에 View 로 얹습니다.
1027
+ */
1028
+ override fun openMediaGallery(options: ReadableMap, promise: Promise) {
1029
+ val activity = getCurrentActivity()
1030
+ if (activity == null) {
1031
+ promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
1032
+ return
1033
+ }
1034
+ val o: Map<String, Any?> = options.toHashMap()
1035
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
1036
+ try {
1037
+ val systemDark = (activity.resources.configuration.uiMode and
1038
+ android.content.res.Configuration.UI_MODE_NIGHT_MASK) == android.content.res.Configuration.UI_MODE_NIGHT_YES
1039
+ val config = MediaGalleryConfig(o, systemDark)
1040
+ val galleryId = config.galleryId
1041
+ if (mediaGalleries[galleryId]?.isShowing == true) {
1042
+ promise.reject("E_GALLERY_OPEN", "Media gallery '$galleryId' is already open")
1043
+ return@runOnUiThread
1044
+ }
1045
+ MediaGalleryRegistry.ensureDemo(activity.applicationContext)
1046
+ val dataSourceName = o["dataSource"] as? String
1047
+ val store: MediaGalleryStore
1048
+ if (!dataSourceName.isNullOrEmpty()) {
1049
+ val dataSource = MediaGalleryRegistry.source(dataSourceName)
1050
+ if (dataSource == null) {
1051
+ promise.reject("E_NO_DATA_SOURCE", "Media gallery data source '$dataSourceName' is not registered")
1052
+ return@runOnUiThread
1053
+ }
1054
+ @Suppress("UNCHECKED_CAST")
1055
+ val params = (o["dataSourceParams"] as? Map<String, Any?>) ?: emptyMap()
1056
+ store = MediaGalleryStore(galleryId, MediaGalleryStore.Source.Native(dataSource, params))
1057
+ } else {
1058
+ store = MediaGalleryStore(
1059
+ galleryId,
1060
+ MediaGalleryStore.Source.JavaScript,
1061
+ MediaGalleryItem.listFrom(o["initialItems"]),
1062
+ o["nextCursor"] as? String,
1063
+ o["hasMore"] as? Boolean ?: false
1064
+ )
1065
+ store.onNeedPage = { cursor ->
1066
+ emitGalleryEvent("onGalleryNeedPage", Arguments.createMap().apply {
1067
+ putString("galleryId", galleryId)
1068
+ if (cursor != null) putString("cursor", cursor) else putNull("cursor")
1069
+ })
1070
+ }
1071
+ }
1072
+ val overlay = MediaGalleryOverlay(activity, store, config)
1073
+ overlay.onClosed = {
1074
+ if (mediaGalleries[galleryId] === overlay) mediaGalleries.remove(galleryId)
1075
+ emitGalleryEvent("onGalleryClosed", Arguments.createMap().apply { putString("galleryId", galleryId) })
1076
+ }
1077
+ // 뷰어에서 삭제를 고르면: 모듈은 그 묶음을 그리드·뷰어에서 지우고, 실제 삭제는 앱이 하도록 이벤트만 보냄
1078
+ overlay.onItemDeleted = { item, option ->
1079
+ emitGalleryEvent("onGalleryItemDeleted", Arguments.createMap().apply {
1080
+ putString("galleryId", galleryId)
1081
+ putString("id", item.id)
1082
+ putString("groupId", item.groupId)
1083
+ if (option != null) putString("option", option) else putNull("option")
1084
+ })
1085
+ }
1086
+ // 상단 바 메뉴에서 그리드 크기를 바꾸면 (모듈이 기기에 저장, 앱에도 알림)
1087
+ overlay.onGridSizeChanged = { columns ->
1088
+ emitGalleryEvent("onGalleryGridSizeChanged", Arguments.createMap().apply {
1089
+ putString("galleryId", galleryId)
1090
+ putString("gridSize", MediaGalleryGridPreference.gridSizeString(columns))
1091
+ })
1092
+ }
1093
+ mediaGalleries[galleryId] = overlay
1094
+ overlay.show()
1095
+ promise.resolve(Arguments.createMap().apply { putString("galleryId", galleryId) })
1096
+ } catch (e: Exception) {
1097
+ promise.reject("E_MEDIA_GALLERY", "Failed to open media gallery: ${e.message}", e)
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ override fun closeMediaGallery(galleryId: String, promise: Promise) {
1103
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
1104
+ mediaGalleries[galleryId]?.close(includeViewer = true)
1105
+ promise.resolve(null)
1106
+ }
1107
+ }
1108
+
1109
+ /** JS 공급 방식: { galleryId, items, nextCursor?, hasMore } */
1110
+ override fun appendGalleryItems(payload: ReadableMap, promise: Promise) {
1111
+ val p: Map<String, Any?> = payload.toHashMap()
1112
+ val galleryId = p["galleryId"] as? String ?: ""
1113
+ val page = MediaGalleryPage(
1114
+ MediaGalleryItem.listFrom(p["items"]),
1115
+ p["nextCursor"] as? String,
1116
+ p["hasMore"] as? Boolean ?: false
1117
+ )
1118
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
1119
+ mediaGalleries[galleryId]?.store?.appendExternal(page)
1120
+ promise.resolve(null)
1121
+ }
1122
+ }
1123
+
1124
+ /** 앱이 다른 경로로 지운 묶음 반영: { galleryId, groupIds } */
1125
+ override fun removeGalleryItems(payload: ReadableMap, promise: Promise) {
1126
+ val p: Map<String, Any?> = payload.toHashMap()
1127
+ val galleryId = p["galleryId"] as? String ?: ""
1128
+ val groupIds = (p["groupIds"] as? List<*> ?: emptyList<Any?>()).mapNotNull { v ->
1129
+ when (v) {
1130
+ is String -> v
1131
+ is Number -> v.toDouble().let { d -> if (d % 1.0 == 0.0) d.toLong().toString() else d.toString() }
1132
+ else -> null
1133
+ }
1134
+ }.toSet()
1135
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
1136
+ mediaGalleries[galleryId]?.store?.removeGroups(groupIds)
1137
+ promise.resolve(null)
1138
+ }
1139
+ }
1140
+
1141
+ override fun invalidate() {
1142
+ super.invalidate()
1143
+ com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
1144
+ val open = mediaGalleries.values.toList()
1145
+ mediaGalleries.clear()
1146
+ open.forEach { it.dismissImmediately() }
1147
+ }
1148
+ reactApplicationContext.removeActivityEventListener(this)
1149
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
1150
+ .unregisterReceiver(pageChangeReceiver)
1151
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
1152
+ .unregisterReceiver(viewerOpenedReceiver)
1153
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
1154
+ .unregisterReceiver(viewerWillCloseReceiver)
1155
+ androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
1156
+ .unregisterReceiver(imageDeletedReceiver)
1157
+ }
1158
+ }
1159
+