react-native-attachment-kit 1.0.17 → 1.1.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 (30) hide show
  1. package/CHANGELOG.md +472 -22
  2. package/android/build.gradle +8 -3
  3. package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
  4. package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +60 -341
  5. package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +60 -341
  6. package/ios/RNAttachmentKit.swift +66 -771
  7. package/ios/RNAttachmentKit.xcframework/Info.plist +5 -5
  8. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
  9. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +6853 -669
  10. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +108 -0
  11. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  12. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftinterface +108 -0
  13. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
  14. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
  15. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +12 -12
  16. package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
  17. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Assets.car +0 -0
  18. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +6853 -669
  19. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +108 -0
  20. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  21. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +108 -0
  22. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +6853 -669
  23. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +108 -0
  24. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  25. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +108 -0
  26. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/RNAttachmentKit +0 -0
  27. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
  28. package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +22 -22
  29. package/package.json +1 -1
  30. package/src/types.ts +5 -0
@@ -46,8 +46,6 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
46
46
  private var lastMaxSelection: Int = 10
47
47
  private var lastThemeColorInt: Int? = null
48
48
  private var lastLanguageCode: String = "en"
49
- @Volatile private var currentVideoExporter: com.rnattachmentkit.VideoExporter? = null
50
- @Volatile private var videoCancelled = false
51
49
  /** 진행 중인 전송 처리 (processingId → 작업). 보내는 중에 갤러리를 다시 열어 또 보낼 수 있어 여러 개일 수 있음 */
52
50
  private val activeProcessingJobs = java.util.concurrent.ConcurrentHashMap<String, com.rnattachmentkit.ProcessingJob>()
53
51
  /** 가장 최근에 시작한 처리 (cancelVideoProcessing 에 processingId 가 없을 때) */
@@ -708,6 +706,8 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
708
706
  // 네이티브 쪽 로케일 리소스(values-ko/ja/zh, 그 외 en)가 자동 적용됩니다.
709
707
  putBoolean("showActionButtons", !options.hasKey("showActionButtons") || options.getBoolean("showActionButtons"))
710
708
  putBoolean("showDeleteButton", !options.hasKey("showDeleteButton") || options.getBoolean("showDeleteButton"))
709
+ // 처음에 상단바·하단바·동영상 조작 UI 를 숨긴 채로 엶 (탭하면 보임)
710
+ putBoolean("startWithUiHidden", options.hasKey("startWithUiHidden") && options.getBoolean("startWithUiHidden"))
711
711
  for (key in listOf(
712
712
  "saveAllText", "saveOneText", "shareAllText", "shareOneText", "actionCancelText",
713
713
  "deleteConfirmTitle", "deleteConfirmMessage", "deleteConfirmGroupMessage", "deleteButtonText"
@@ -967,374 +967,93 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
967
967
  }
968
968
  }
969
969
 
970
+ /** 고른 뒤 처리 — com.rnattachmentkit.AttachmentProcessor (앱 네이티브 화면과 같은 코드). 여기선 RN 이벤트·HUD 만 */
970
971
  private suspend fun processImages(uris: List<Uri>, snap: SendSnapshot): List<com.facebook.react.bridge.WritableMap> = withContext(Dispatchers.IO) {
971
- // 보낸 순간의 설정 (모듈 필드와 같은 이름으로 가림 — 아래 코드는 그대로)
972
- val maxWidth = snap.maxWidth
973
- val maxHeight = snap.maxHeight
974
- val outputFormat = snap.outputFormat
975
- val videoOptions = snap.videoOptions
976
- val lastVideoEditStates = snap.states
977
- val showVideoHUD = snap.showHUD
978
- val lastThemeColorInt = snap.themeColor
979
- val lastLanguageCode = snap.language
980
- val thumbnailOptions = snap.thumbnail
981
972
  val processingId = java.util.UUID.randomUUID().toString()
982
- val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
983
- val contentResolver = reactApplicationContext.contentResolver
984
973
  val totalCount = uris.size
985
- var currentIndex = 0
986
-
987
- // 동영상이 섞여 있으면 진행 HUD 를 띄우고, 동영상은 VideoExporter 로 (사진 처리와 같은 루프)
988
- val isVideoUri: (Uri) -> Boolean = { u ->
989
- u.toString().contains("/video/") || (try { contentResolver.getType(u)?.startsWith("video/") == true } catch (_: Exception) { false })
990
- }
991
- val hasVideo = uris.any(isVideoUri)
992
- videoCancelled = false
974
+ val options = com.rnattachmentkit.AttachmentProcessor.Options(
975
+ maxWidth = snap.maxWidth, maxHeight = snap.maxHeight, outputFormat = snap.outputFormat,
976
+ thumbnail = snap.thumbnail, videoOptions = snap.videoOptions,
977
+ )
978
+ val hasVideo = uris.any { com.rnattachmentkit.AttachmentProcessor.isVideoUri(reactApplicationContext, it) }
979
+ // 앱의 cancelVideoProcessing(index) → job.cancel, HUD 의 취소 → job.abort (전체 취소)
993
980
  val job = com.rnattachmentkit.ProcessingJob()
994
981
  activeProcessingJobs[processingId] = job
995
982
  lastProcessingId = processingId
996
- // ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
997
- try {
998
- val items = Arguments.createArray()
999
- uris.forEachIndexed { i, u ->
1000
- val it0 = if (isVideoUri(u)) {
1001
- val st = lastVideoEditStates[u.toString()] ?: com.rnattachmentkit.VideoEditState(qualityHeight = videoOptions.defaultQuality)
1002
- com.rnattachmentkit.MediaProcessing.videoItem(reactApplicationContext, i, u, st, videoOptions)
1003
- } else {
1004
- com.rnattachmentkit.MediaProcessing.imageItem(reactApplicationContext, i, u, maxWidth, maxHeight)
1005
- }
1006
- items.pushMap(Arguments.createMap().apply {
1007
- putInt("index", it0.index)
1008
- putString("mediaType", if (it0.isVideo) "video" else "image")
1009
- putString("originalUri", it0.originalUri)
1010
- putInt("width", it0.width)
1011
- putInt("height", it0.height)
1012
- it0.durationSec?.let { d -> putDouble("duration", d) }
1013
- it0.thumbnailUri?.let { t ->
1014
- putString("thumbnailUri", t)
1015
- putInt("thumbnailWidth", it0.thumbnailWidth)
1016
- putInt("thumbnailHeight", it0.thumbnailHeight)
1017
- }
1018
- })
1019
- }
1020
- emitGalleryEvent("onProcessingStart", Arguments.createMap().apply {
1021
- putString("processingId", processingId)
1022
- putInt("total", totalCount)
1023
- putArray("items", items)
1024
- })
1025
- } catch (e: Exception) {
1026
- android.util.Log.w("RNAttachmentKit", "onProcessingStart failed: ${e.message}")
1027
- }
1028
983
  var hud: com.rnattachmentkit.VideoExportHUD? = null
1029
- if (hasVideo && showVideoHUD) {
984
+ if (hasVideo && snap.showHUD) {
1030
985
  withContext(Dispatchers.Main) {
1031
986
  val act = getCurrentActivity() ?: return@withContext
1032
- hud = com.rnattachmentkit.VideoExportHUD(act, lastThemeColorInt, lastLanguageCode).also { h ->
1033
- h.onCancel = {
1034
- videoCancelled = true
1035
- currentVideoExporter?.cancel()
1036
- }
987
+ hud = com.rnattachmentkit.VideoExportHUD(act, snap.themeColor, snap.language).also { h ->
988
+ h.onCancel = { job.abort() }
1037
989
  h.show()
1038
990
  }
1039
991
  }
1040
992
  }
1041
- val videoFiles = mutableListOf<File>()
1042
-
993
+ val main = android.os.Handler(android.os.Looper.getMainLooper())
994
+ val emitter = { reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) }
1043
995
  try {
1044
- for (uri in uris) {
1045
- if (videoCancelled) throw com.rnattachmentkit.VideoExporter.CancelledException()
1046
- if (isVideoUri(uri)) {
1047
- val idx = currentIndex
1048
- val exporter = com.rnattachmentkit.VideoExporter(reactApplicationContext)
1049
- currentVideoExporter = exporter
1050
- val cancelledMap = {
1051
- WritableNativeMap().apply {
1052
- putString("originalUri", uri.toString())
1053
- putString("mediaType", "video")
1054
- putString("error", "CANCELLED")
1055
- putBoolean("cancelled", true)
1056
- }
1057
- }
1058
- val resultMap = try {
1059
- // 앱이 이미 취소한 동영상은 건너뜀
1060
- if (job.isCancelled(idx)) throw com.rnattachmentkit.VideoExporter.CancelledException()
1061
- job.begin(idx, exporter)
1062
- val state = lastVideoEditStates[uri.toString()] ?: com.rnattachmentkit.VideoEditState(qualityHeight = videoOptions.defaultQuality)
1063
- val throttle = com.rnattachmentkit.ProgressThrottle()
1064
- val onProgress: (Float) -> Unit = { p ->
1065
- hud?.let { h -> android.os.Handler(android.os.Looper.getMainLooper()).post { h.update(p, idx, totalCount) } }
1066
- if (throttle.shouldSend(p)) emitGalleryEvent("onVideoProgress", Arguments.createMap().apply {
1067
- putInt("index", idx); putInt("total", totalCount); putDouble("progress", p.toDouble())
996
+ com.rnattachmentkit.AttachmentProcessor.process(reactApplicationContext, uris, snap.states, options, job,
997
+ object : com.rnattachmentkit.AttachmentProcessor.Listener {
998
+ // ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
999
+ override fun onStart(items: List<com.rnattachmentkit.MediaProcessing.StartItem>) {
1000
+ val arr = Arguments.createArray()
1001
+ items.forEach { it0 ->
1002
+ arr.pushMap(Arguments.createMap().apply {
1003
+ putInt("index", it0.index)
1004
+ putString("mediaType", if (it0.isVideo) "video" else "image")
1005
+ putString("originalUri", it0.originalUri)
1006
+ putInt("width", it0.width)
1007
+ putInt("height", it0.height)
1008
+ it0.durationSec?.let { d -> putDouble("duration", d) }
1009
+ it0.thumbnailUri?.let { t ->
1010
+ putString("thumbnailUri", t)
1011
+ putInt("thumbnailWidth", it0.thumbnailWidth)
1012
+ putInt("thumbnailHeight", it0.thumbnailHeight)
1013
+ }
1014
+ })
1015
+ }
1016
+ emitGalleryEvent("onProcessingStart", Arguments.createMap().apply {
1068
1017
  putString("processingId", processingId)
1018
+ putInt("total", totalCount)
1019
+ putArray("items", arr)
1069
1020
  })
1070
1021
  }
1071
- // 편집 화면에서 미리 만들어 둔 파일이 있으면(또는 만드는 중이면 끝나고) 그대로 씀
1072
- // 기다리는 동안 앱(X)이나 HUD 가 취소하면 바로 멈춤 (전엔 미리 만들기가 끝날 때까지 못 빠져나왔음)
1073
- val stop = { videoCancelled || job.isCancelled(idx) }
1074
- val res = com.rnattachmentkit.VideoPrepareManager.takeReady(uri, state, onProgress, stop)
1075
- ?: run {
1076
- if (stop()) throw com.rnattachmentkit.VideoExporter.CancelledException()
1077
- exporter.export(uri, state, videoOptions, reactApplicationContext.cacheDir, "turbo_video_${System.currentTimeMillis()}_$idx", onProgress)
1078
- }
1079
- withContext(Dispatchers.Main) { hud?.update(1f, idx, totalCount) }
1080
- // 미리 만든 파일을 기다리는 동안 취소됐으면 결과를 쓰지 않음
1081
- if (job.isCancelled(idx)) throw com.rnattachmentkit.VideoExporter.CancelledException()
1082
- videoFiles += res.file
1083
- res.thumbnailFile?.let { videoFiles += it }
1084
- WritableNativeMap().apply {
1085
- putString("uri", Uri.fromFile(res.file).toString())
1086
- putString("originalUri", uri.toString())
1087
- putString("mediaType", "video")
1088
- putString("type", "video/mp4")
1089
- putInt("width", res.width)
1090
- putInt("height", res.height)
1091
- putDouble("duration", res.durationMs / 1000.0)
1092
- putDouble("fileSize", res.fileSize.toDouble())
1093
- putString("fileName", res.file.name)
1094
- putString("fileExtension", "mp4")
1095
- putString("videoCodec", res.codec)
1096
- putInt("videoBitrate", res.bitrate)
1097
- putBoolean("edited", res.edited)
1098
- if (res.thumbnailFile != null) {
1099
- putString("thumbnailUri", Uri.fromFile(res.thumbnailFile).toString())
1100
- putInt("thumbnailWidth", res.thumbnailWidth)
1101
- putInt("thumbnailHeight", res.thumbnailHeight)
1102
- }
1103
- }
1104
- } catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
1105
- // 앱이 이 동영상만 취소 → 나머지는 계속. HUD 취소면 전체 취소
1106
- if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else throw e
1107
- } catch (e: Exception) {
1108
- // 취소하면서 변환기가 다른 예외로 끝날 수도 있음 → 취소로 처리
1109
- if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else {
1110
- android.util.Log.w("RNAttachmentKit", "video export failed: ${e.message}", e)
1111
- WritableNativeMap().apply {
1112
- putString("originalUri", uri.toString())
1113
- putString("mediaType", "video")
1114
- putString("error", e.message ?: "export failed")
1115
- }
1116
- }
1117
- } finally {
1118
- currentVideoExporter = null
1119
- job.end()
1120
- }
1121
- results.add(resultMap)
1122
- try {
1123
- val copy = WritableNativeMap().apply { merge(resultMap) }
1124
- reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
1125
- .emit("onImageProcessed", WritableNativeMap().apply {
1126
- putInt("total", totalCount); putInt("index", idx); putMap("image", copy)
1022
+
1023
+ override fun onVideoProgress(index: Int, progress: Float) {
1024
+ hud?.let { h -> main.post { h.update(progress, index, totalCount) } }
1025
+ emitGalleryEvent("onVideoProgress", Arguments.createMap().apply {
1026
+ putInt("index", index); putInt("total", totalCount); putDouble("progress", progress.toDouble())
1127
1027
  putString("processingId", processingId)
1128
1028
  })
1129
- } catch (_: Exception) {}
1130
- currentIndex++
1131
- continue
1132
- }
1133
- try {
1134
- // 1. Get EXIF Orientation
1135
- var orientation = android.media.ExifInterface.ORIENTATION_NORMAL
1136
- try {
1137
- contentResolver.openInputStream(uri)?.use { exifInputStream ->
1138
- val exif = android.media.ExifInterface(exifInputStream)
1139
- orientation = exif.getAttributeInt(android.media.ExifInterface.TAG_ORIENTATION, android.media.ExifInterface.ORIENTATION_NORMAL)
1140
1029
  }
1141
- } catch (e: Exception) {}
1142
1030
 
1143
- contentResolver.openInputStream(uri)?.use { inputStream ->
1144
- // Get original image dimensions
1145
- val originalOptions = BitmapFactory.Options().apply {
1146
- inJustDecodeBounds = true
1147
- }
1148
- BitmapFactory.decodeStream(inputStream, null, originalOptions)
1149
-
1150
- var originalWidth = originalOptions.outWidth
1151
- var originalHeight = originalOptions.outHeight
1152
-
1153
- // Swap width and height if rotated by 90 or 270 degrees
1154
- if (orientation == android.media.ExifInterface.ORIENTATION_ROTATE_90 || orientation == android.media.ExifInterface.ORIENTATION_ROTATE_270) {
1155
- val temp = originalWidth
1156
- originalWidth = originalHeight
1157
- originalHeight = temp
1158
- }
1159
-
1160
- // Load and resize image
1161
- contentResolver.openInputStream(uri)?.use { resizedInputStream ->
1162
- // Initial decode for sizing
1163
- val decodeOptions = BitmapFactory.Options().apply {
1164
- inSampleSize = calculateInSampleSize(originalWidth, originalHeight, maxWidth, maxHeight)
1165
- }
1166
- var bitmap = BitmapFactory.decodeStream(resizedInputStream, null, decodeOptions)
1167
-
1168
- if (bitmap != null) {
1169
- // Apply EXIF Rotation
1170
- val matrix = android.graphics.Matrix()
1171
- when (orientation) {
1172
- android.media.ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
1173
- android.media.ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
1174
- android.media.ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
1175
- android.media.ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1f, 1f)
1176
- android.media.ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1f, -1f)
1177
- }
1178
-
1179
- if (!matrix.isIdentity) {
1180
- val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
1181
- if (rotatedBitmap != bitmap) {
1182
- bitmap.recycle()
1183
- bitmap = rotatedBitmap
1184
- }
1185
- }
1186
-
1187
- // Precise resize if needed
1188
- val finalBitmap = if (bitmap.width > maxWidth || bitmap.height > maxHeight) {
1189
- resizeBitmap(bitmap, maxWidth, maxHeight)
1190
- } else {
1191
- bitmap
1192
- }
1193
-
1194
- val processedBitmap = ImageProcessor.applyWatermarkIfNeeded(reactApplicationContext, finalBitmap)
1195
-
1196
- // Determine format and extension
1197
- val compressFormat: Bitmap.CompressFormat
1198
- val extension: String
1199
- val mimeType: String
1200
-
1201
- when (outputFormat) {
1202
- "png" -> {
1203
- compressFormat = Bitmap.CompressFormat.PNG
1204
- extension = "png"
1205
- mimeType = "image/png"
1206
- }
1207
- "jpg", "jpeg" -> {
1208
- compressFormat = Bitmap.CompressFormat.JPEG
1209
- extension = "jpg"
1210
- mimeType = "image/jpeg"
1211
- }
1212
- else -> { // webp
1213
- compressFormat = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
1214
- Bitmap.CompressFormat.WEBP_LOSSY
1215
- } else {
1216
- @Suppress("DEPRECATION")
1217
- Bitmap.CompressFormat.WEBP
1218
- }
1219
- extension = "webp"
1220
- mimeType = "image/webp"
1221
- }
1222
- }
1223
-
1224
- // Save to temporary file
1225
- val tempFile = File.createTempFile("turbo_edited_${System.currentTimeMillis()}_", ".$extension", reactApplicationContext.cacheDir)
1226
- FileOutputStream(tempFile).use { out ->
1227
- processedBitmap.compress(compressFormat, 85, out)
1228
- }
1229
- // 썸네일: 같은 비트맵(방향·크롭·그리기 반영)에서 — 원본을 다시 읽지 않음
1230
- val thumb = thumbnailOptions?.let {
1231
- com.rnattachmentkit.PhotoThumbnail.make(processedBitmap, it, reactApplicationContext.cacheDir, currentIndex)
1232
- }
1233
-
1234
- val finalWidth = processedBitmap.width
1235
- val finalHeight = processedBitmap.height
1236
-
1237
- // Cleanup
1238
- if (bitmap != processedBitmap) {
1239
- bitmap.recycle()
1240
- }
1241
- if (finalBitmap != processedBitmap && finalBitmap != bitmap) {
1242
- finalBitmap.recycle()
1243
- }
1244
- processedBitmap.recycle()
1245
-
1246
- val createImageInfo = {
1247
- WritableNativeMap().apply {
1248
- putString("uri", Uri.fromFile(tempFile).toString())
1249
- putInt("width", finalWidth)
1250
- putInt("height", finalHeight)
1251
- putString("type", mimeType)
1252
- putString("fileName", tempFile.name)
1253
- putString("fileExtension", extension)
1254
- putDouble("fileSize", tempFile.length().toDouble())
1255
- putString("originalUri", uri.toString())
1256
- putInt("originalWidth", originalWidth)
1257
- putInt("originalHeight", originalHeight)
1258
- if (thumb != null) {
1259
- putString("thumbnailUri", Uri.fromFile(thumb.file).toString())
1260
- putInt("thumbnailWidth", thumb.width)
1261
- putInt("thumbnailHeight", thumb.height)
1262
- putDouble("thumbnailFileSize", thumb.fileSize.toDouble())
1263
- }
1264
- }
1265
- }
1266
- val imageInfo = createImageInfo()
1267
- results.add(imageInfo)
1268
-
1269
- try {
1270
- val eventMap = WritableNativeMap().apply {
1271
- putInt("total", totalCount)
1272
- putInt("index", currentIndex)
1273
- putString("processingId", processingId)
1274
- putMap("image", createImageInfo())
1275
- }
1276
- reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
1277
- .emit("onImageProcessed", eventMap)
1278
- } catch (e: Exception) { }
1279
- }
1031
+ override fun onItem(result: com.rnattachmentkit.AttachmentProcessor.Result) {
1032
+ if (result.isVideo) hud?.let { h -> main.post { h.update(1f, result.index, totalCount) } }
1033
+ try {
1034
+ emitter().emit("onImageProcessed", WritableNativeMap().apply {
1035
+ putInt("total", totalCount); putInt("index", result.index); putMap("image", toWritableMap(result))
1036
+ putString("processingId", processingId)
1037
+ })
1038
+ } catch (_: Exception) {}
1280
1039
  }
1281
- }
1282
- currentIndex++
1283
- } catch (e: Exception) {
1284
- currentIndex++
1285
- // Skip failed images
1286
- continue
1287
- }
1288
- }
1289
- } catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
1290
- // 취소: 지금까지 만든 동영상 파일은 지움
1291
- videoFiles.forEach { try { it.delete() } catch (_: Exception) {} }
1292
- throw e
1040
+ }).map { toWritableMap(it) }
1293
1041
  } finally {
1294
1042
  activeProcessingJobs.remove(processingId)
1295
1043
  if (hud != null) withContext(Dispatchers.Main) { hud?.dismiss() }
1296
1044
  }
1297
-
1298
- results
1299
1045
  }
1300
-
1301
- private fun calculateInSampleSize(
1302
- reqWidth: Int,
1303
- reqHeight: Int,
1304
- maxWidth: Int,
1305
- maxHeight: Int
1306
- ): Int {
1307
- var inSampleSize = 1
1308
-
1309
- if (reqHeight > maxHeight || reqWidth > maxWidth) {
1310
- val halfHeight = reqHeight / 2
1311
- val halfWidth = reqWidth / 2
1312
-
1313
- while ((halfHeight / inSampleSize) >= maxHeight && (halfWidth / inSampleSize) >= maxWidth) {
1314
- inSampleSize *= 2
1046
+
1047
+ /** AttachmentProcessor.Result → RN 결과 맵 (예전 브릿지와 같은 필드·타입) */
1048
+ private fun toWritableMap(r: com.rnattachmentkit.AttachmentProcessor.Result): WritableMap = WritableNativeMap().apply {
1049
+ r.toMap().forEach { (k, v) ->
1050
+ when (v) {
1051
+ is Boolean -> putBoolean(k, v)
1052
+ is Int -> putInt(k, v)
1053
+ is Number -> putDouble(k, v.toDouble())
1054
+ else -> putString(k, v.toString())
1315
1055
  }
1316
1056
  }
1317
-
1318
- return inSampleSize
1319
- }
1320
-
1321
- private fun resizeBitmap(bitmap: Bitmap, maxWidth: Int, maxHeight: Int): Bitmap {
1322
- val width = bitmap.width
1323
- val height = bitmap.height
1324
-
1325
- val ratioBitmap = width.toFloat() / height.toFloat()
1326
- val ratioMax = maxWidth.toFloat() / maxHeight.toFloat()
1327
-
1328
- var finalWidth = maxWidth
1329
- var finalHeight = maxHeight
1330
-
1331
- if (ratioMax > ratioBitmap) {
1332
- finalWidth = (maxHeight * ratioBitmap).toInt()
1333
- } else {
1334
- finalHeight = (maxWidth / ratioBitmap).toInt()
1335
- }
1336
-
1337
- return Bitmap.createScaledBitmap(bitmap, finalWidth, finalHeight, true)
1338
1057
  }
1339
1058
 
1340
1059