react-native-attachment-kit 1.0.5 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +100 -3
- package/android/build.gradle +14 -0
- package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
- package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +280 -13
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +282 -13
- package/ios/RNAttachmentKit.m +18 -0
- package/ios/RNAttachmentKit.swift +447 -9
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Headers/RNAttachmentKit-Swift.h +21 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Info.plist +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +33440 -7566
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +193 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftinterface +193 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +15 -15
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Headers/RNAttachmentKit-Swift.h +42 -10
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Info.plist +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +33440 -7566
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +193 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +193 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +33440 -7566
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +193 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +193 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +25 -25
- package/package.json +2 -2
- package/react-native-attachment-kit.podspec +1 -1
- package/src/NativeAttachmentKit.ts +5 -0
- package/src/index.ts +118 -7
- package/src/types.ts +173 -0
|
@@ -39,6 +39,33 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
39
39
|
private var maxWidth: Int = 1024
|
|
40
40
|
private var maxHeight: Int = 1024
|
|
41
41
|
private var outputFormat: String = "webp" // Default to webp
|
|
42
|
+
// ─── 동영상 (openGallery mediaType / video 옵션) ───
|
|
43
|
+
private var mediaType: String = "photo"
|
|
44
|
+
private var videoOptions = com.rnattachmentkit.VideoExportOptions()
|
|
45
|
+
/** onSelectionChange 에 실어 보낼 maxSelection (예전엔 10 고정) */
|
|
46
|
+
private var lastMaxSelection: Int = 10
|
|
47
|
+
private var lastThemeColorInt: Int? = null
|
|
48
|
+
private var lastLanguageCode: String = "en"
|
|
49
|
+
@Volatile private var currentVideoExporter: com.rnattachmentkit.VideoExporter? = null
|
|
50
|
+
@Volatile private var videoCancelled = false
|
|
51
|
+
/** 진행 중인 전송 처리 (processingId → 작업). 보내는 중에 갤러리를 다시 열어 또 보낼 수 있어 여러 개일 수 있음 */
|
|
52
|
+
private val activeProcessingJobs = java.util.concurrent.ConcurrentHashMap<String, com.rnattachmentkit.ProcessingJob>()
|
|
53
|
+
/** 가장 최근에 시작한 처리 (cancelVideoProcessing 에 processingId 가 없을 때) */
|
|
54
|
+
@Volatile private var lastProcessingId: String? = null
|
|
55
|
+
|
|
56
|
+
/** 보낸 순간의 설정 — 처리 중에 갤러리를 다시 열면 모듈 필드가 새 호출 값으로 바뀌므로, 처리는 이 사본만 씀 */
|
|
57
|
+
private class SendSnapshot(
|
|
58
|
+
val maxWidth: Int, val maxHeight: Int, val outputFormat: String,
|
|
59
|
+
val videoOptions: com.rnattachmentkit.VideoExportOptions,
|
|
60
|
+
val states: Map<String, com.rnattachmentkit.VideoEditState>,
|
|
61
|
+
val showHUD: Boolean, val themeColor: Int?, val language: String
|
|
62
|
+
)
|
|
63
|
+
private fun sendSnapshot() = SendSnapshot(maxWidth, maxHeight, outputFormat, videoOptions, HashMap(lastVideoEditStates),
|
|
64
|
+
showVideoHUD, lastThemeColorInt, lastLanguageCode)
|
|
65
|
+
/** 동영상 변환 진행 HUD — video.showProgressHUD, 없으면 asyncProcessing 이 아닐 때만 (이벤트로 말풍선에 그리는 앱은 HUD 없이) */
|
|
66
|
+
private var showVideoHUD = true
|
|
67
|
+
/** 시트가 닫힐 때 넘겨받은 동영상 편집 상태 (원본 uri 문자열 → 품질·음소거) */
|
|
68
|
+
private var lastVideoEditStates: Map<String, com.rnattachmentkit.VideoEditState> = emptyMap()
|
|
42
69
|
|
|
43
70
|
@ReactMethod
|
|
44
71
|
fun addListener(eventName: String) {
|
|
@@ -255,6 +282,22 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
255
282
|
false
|
|
256
283
|
}
|
|
257
284
|
|
|
285
|
+
// 동영상: mediaType "photo"(기본) | "video" | "all", video: VideoOptions
|
|
286
|
+
mediaType = (if (options.hasKey("mediaType") && !options.isNull("mediaType")) options.getString("mediaType") else null)
|
|
287
|
+
?.lowercase()?.takeIf { it == "video" || it == "all" } ?: "photo"
|
|
288
|
+
videoOptions = com.rnattachmentkit.VideoExportOptions.fromMap(
|
|
289
|
+
if (options.hasKey("video") && !options.isNull("video")) options.getMap("video")?.toHashMap() else null
|
|
290
|
+
)
|
|
291
|
+
run {
|
|
292
|
+
val async = options.hasKey("asyncProcessing") && !options.isNull("asyncProcessing") && options.getBoolean("asyncProcessing")
|
|
293
|
+
val v = if (options.hasKey("video") && !options.isNull("video")) options.getMap("video") else null
|
|
294
|
+
showVideoHUD = if (v != null && v.hasKey("showProgressHUD") && !v.isNull("showProgressHUD")) v.getBoolean("showProgressHUD") else !async
|
|
295
|
+
}
|
|
296
|
+
lastMaxSelection = maxSelection
|
|
297
|
+
TurboImagePickerConfig.videoOptions = videoOptions
|
|
298
|
+
lastLanguageCode = languageCode
|
|
299
|
+
lastThemeColorInt = try { themeColor?.let { android.graphics.Color.parseColor(it) } } catch (_: Exception) { null }
|
|
300
|
+
|
|
258
301
|
val fragmentActivity = activity as? FragmentActivity
|
|
259
302
|
if (fragmentActivity == null) {
|
|
260
303
|
promise.reject(E_NO_ACTIVITY, "Activity is not a FragmentActivity")
|
|
@@ -283,6 +326,12 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
283
326
|
} else {
|
|
284
327
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
285
328
|
}
|
|
329
|
+
// Android 13+ 에서 동영상도 보이면 READ_MEDIA_VIDEO 를 같이 요청
|
|
330
|
+
val mediaPermissions: Array<String> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && mediaType != "photo") {
|
|
331
|
+
arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO)
|
|
332
|
+
} else {
|
|
333
|
+
arrayOf(permission)
|
|
334
|
+
}
|
|
286
335
|
|
|
287
336
|
val showSheet = {
|
|
288
337
|
fragmentActivity.runOnUiThread {
|
|
@@ -299,7 +348,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
299
348
|
options.getDouble("attachMenuMinBottomMargin").toFloat() else com.rnattachmentkit.AttachMenuBarView.BOTTOM_MARGIN_DP.toFloat(),
|
|
300
349
|
// 위치 보내기 때 지도 이미지 캡처 여부. 기본 true (기존 그대로). false 면 이미지 없이 위치 정보만
|
|
301
350
|
captureLocationImage = !(options.hasKey("captureLocationImage") && !options.isNull("captureLocationImage")) ||
|
|
302
|
-
options.getBoolean("captureLocationImage")
|
|
351
|
+
options.getBoolean("captureLocationImage"),
|
|
352
|
+
// 동영상 (mediaType "video"/"all" 일 때 그리드에 동영상, 길이 제한)
|
|
353
|
+
mediaType = mediaType,
|
|
354
|
+
maxVideoDurationSec = videoOptions.maxDurationSec
|
|
303
355
|
)
|
|
304
356
|
// 위치 탭에서 보내기 → onLocationSelected 이벤트로 결과를 넘기고, openGallery 는 빈 배열로 끝냄
|
|
305
357
|
bottomSheet.setOnLocationSelectedListener { loc ->
|
|
@@ -345,6 +397,7 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
345
397
|
settle { it.resolve(com.facebook.react.bridge.Arguments.createArray()) }
|
|
346
398
|
}
|
|
347
399
|
bottomSheet.setOnImagesSelectedListener { uris ->
|
|
400
|
+
lastVideoEditStates = HashMap(bottomSheet.videoEditStates)
|
|
348
401
|
val result = WritableNativeArray()
|
|
349
402
|
for (uri in uris) {
|
|
350
403
|
result.pushString(uri.toString())
|
|
@@ -370,7 +423,7 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
370
423
|
}
|
|
371
424
|
}
|
|
372
425
|
|
|
373
|
-
if (ContextCompat.checkSelfPermission(fragmentActivity,
|
|
426
|
+
if (mediaPermissions.all { ContextCompat.checkSelfPermission(fragmentActivity, it) == PackageManager.PERMISSION_GRANTED }) {
|
|
374
427
|
showSheet()
|
|
375
428
|
} else {
|
|
376
429
|
val permissionAwareActivity = fragmentActivity as? PermissionAwareActivity
|
|
@@ -385,21 +438,19 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
385
438
|
showSheet()
|
|
386
439
|
return
|
|
387
440
|
}
|
|
388
|
-
|
|
441
|
+
mediaPermissions + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
|
|
389
442
|
} else {
|
|
390
|
-
|
|
443
|
+
mediaPermissions
|
|
391
444
|
}
|
|
392
445
|
|
|
393
446
|
permissionAwareActivity.requestPermissions(permissionsToRequest, 100, object : PermissionListener {
|
|
394
447
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray): Boolean {
|
|
395
448
|
if (requestCode == 100 && grantResults != null) {
|
|
396
449
|
val isGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
(grantResults.size > 1 && grantResults[1] == PackageManager.PERMISSION_GRANTED)
|
|
400
|
-
)
|
|
450
|
+
// 14+: 전체 허용이든 "일부 선택"이든 하나라도 허용이면 진행
|
|
451
|
+
grantResults.any { it == PackageManager.PERMISSION_GRANTED }
|
|
401
452
|
} else {
|
|
402
|
-
grantResults.isNotEmpty() && grantResults
|
|
453
|
+
grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
|
403
454
|
}
|
|
404
455
|
|
|
405
456
|
if (isGranted) {
|
|
@@ -462,6 +513,15 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
462
513
|
promise.resolve(null)
|
|
463
514
|
}
|
|
464
515
|
|
|
516
|
+
/** 동영상 변환 취소 (HUD 없이 앱이 말풍선에서). index -1 = 남은 동영상 전부. 진행 중인 처리가 없으면 false */
|
|
517
|
+
@ReactMethod
|
|
518
|
+
fun cancelVideoProcessing(index: Double, processingId: String, promise: Promise) {
|
|
519
|
+
val job = (processingId.ifEmpty { lastProcessingId })?.let { activeProcessingJobs[it] }
|
|
520
|
+
if (job == null) { promise.resolve(false); return }
|
|
521
|
+
job.cancel(if (index >= 0) index.toInt() else null)
|
|
522
|
+
promise.resolve(true)
|
|
523
|
+
}
|
|
524
|
+
|
|
465
525
|
@ReactMethod
|
|
466
526
|
override fun closeGallery(promise: Promise) {
|
|
467
527
|
val currentActivity = currentActivity
|
|
@@ -600,6 +660,22 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
600
660
|
putStringArrayList("placeholderImages", placeholders)
|
|
601
661
|
}
|
|
602
662
|
}
|
|
663
|
+
// 동영상 페이지 (뷰어 동영상 작업 지시서 V1): videos[i] = { uri, cacheFile? } 또는 null(사진).
|
|
664
|
+
// images[i] 는 그 동영상의 포스터. 이 오버레이 뷰어에서만 재생합니다.
|
|
665
|
+
if (options.hasKey("videos")) {
|
|
666
|
+
val videoArray = options.getArray("videos")
|
|
667
|
+
if (videoArray != null) {
|
|
668
|
+
val uris = ArrayList<String>()
|
|
669
|
+
val caches = ArrayList<String>()
|
|
670
|
+
for (i in 0 until videoArray.size()) {
|
|
671
|
+
val m = if (videoArray.getType(i) == com.facebook.react.bridge.ReadableType.Map) videoArray.getMap(i) else null
|
|
672
|
+
uris.add(if (m != null && m.hasKey("uri") && !m.isNull("uri")) m.getString("uri") ?: "" else "")
|
|
673
|
+
caches.add(if (m != null && m.hasKey("cacheFile") && !m.isNull("cacheFile")) m.getString("cacheFile") ?: "" else "")
|
|
674
|
+
}
|
|
675
|
+
putStringArrayList(com.rnattachmentkit.ViewerVideoSource.ARG_URIS, uris)
|
|
676
|
+
putStringArrayList(com.rnattachmentkit.ViewerVideoSource.ARG_CACHE_FILES, caches)
|
|
677
|
+
}
|
|
678
|
+
}
|
|
603
679
|
// 상단바 저장/공유/삭제 버튼과 그 문구들 (iOS와 동일한 옵션 키). 문구를 안 넘기면
|
|
604
680
|
// 네이티브 쪽 로케일 리소스(values-ko/ja/zh, 그 외 en)가 자동 적용됩니다.
|
|
605
681
|
putBoolean("showActionButtons", !options.hasKey("showActionButtons") || options.getBoolean("showActionButtons"))
|
|
@@ -792,9 +868,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
792
868
|
return
|
|
793
869
|
}
|
|
794
870
|
|
|
871
|
+
val snap = sendSnapshot()
|
|
795
872
|
moduleScope.launch {
|
|
796
873
|
try {
|
|
797
|
-
val results = processImages(uris)
|
|
874
|
+
val results = processImages(uris, snap)
|
|
798
875
|
if (results.isNotEmpty()) {
|
|
799
876
|
promise.resolve(results[0])
|
|
800
877
|
} else {
|
|
@@ -838,7 +915,7 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
838
915
|
// Process images asynchronously
|
|
839
916
|
val eventMap = com.facebook.react.bridge.WritableNativeMap()
|
|
840
917
|
eventMap.putInt("selectedCount", uris.size)
|
|
841
|
-
eventMap.putInt("maxSelection",
|
|
918
|
+
eventMap.putInt("maxSelection", lastMaxSelection)
|
|
842
919
|
try {
|
|
843
920
|
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
844
921
|
.emit("onSelectionChange", eventMap)
|
|
@@ -846,25 +923,183 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
846
923
|
// Ignore
|
|
847
924
|
}
|
|
848
925
|
|
|
926
|
+
val snap = sendSnapshot()
|
|
849
927
|
moduleScope.launch {
|
|
850
928
|
try {
|
|
851
|
-
val results = processImages(uris)
|
|
929
|
+
val results = processImages(uris, snap)
|
|
852
930
|
val array = com.facebook.react.bridge.Arguments.createArray()
|
|
853
931
|
results.forEach { array.pushMap(it) }
|
|
854
932
|
actualPromise.resolve(array)
|
|
933
|
+
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
934
|
+
actualPromise.reject("CANCELLED", "Video export cancelled")
|
|
855
935
|
} catch (e: Exception) {
|
|
856
936
|
actualPromise.reject(E_FAILED_TO_PICK, "Failed to process images: ${e.message}", e)
|
|
857
937
|
}
|
|
858
938
|
}
|
|
859
939
|
}
|
|
860
940
|
|
|
861
|
-
private suspend fun processImages(uris: List<Uri
|
|
941
|
+
private suspend fun processImages(uris: List<Uri>, snap: SendSnapshot): List<com.facebook.react.bridge.WritableMap> = withContext(Dispatchers.IO) {
|
|
942
|
+
// 보낸 순간의 설정 (모듈 필드와 같은 이름으로 가림 — 아래 코드는 그대로)
|
|
943
|
+
val maxWidth = snap.maxWidth
|
|
944
|
+
val maxHeight = snap.maxHeight
|
|
945
|
+
val outputFormat = snap.outputFormat
|
|
946
|
+
val videoOptions = snap.videoOptions
|
|
947
|
+
val lastVideoEditStates = snap.states
|
|
948
|
+
val showVideoHUD = snap.showHUD
|
|
949
|
+
val lastThemeColorInt = snap.themeColor
|
|
950
|
+
val lastLanguageCode = snap.language
|
|
951
|
+
val processingId = java.util.UUID.randomUUID().toString()
|
|
862
952
|
val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
|
|
863
953
|
val contentResolver = reactApplicationContext.contentResolver
|
|
864
954
|
val totalCount = uris.size
|
|
865
955
|
var currentIndex = 0
|
|
866
956
|
|
|
957
|
+
// 동영상이 섞여 있으면 진행 HUD 를 띄우고, 동영상은 VideoExporter 로 (사진 처리와 같은 루프)
|
|
958
|
+
val isVideoUri: (Uri) -> Boolean = { u ->
|
|
959
|
+
u.toString().contains("/video/") || (try { contentResolver.getType(u)?.startsWith("video/") == true } catch (_: Exception) { false })
|
|
960
|
+
}
|
|
961
|
+
val hasVideo = uris.any(isVideoUri)
|
|
962
|
+
videoCancelled = false
|
|
963
|
+
val job = com.rnattachmentkit.ProcessingJob()
|
|
964
|
+
activeProcessingJobs[processingId] = job
|
|
965
|
+
lastProcessingId = processingId
|
|
966
|
+
// ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
|
|
967
|
+
try {
|
|
968
|
+
val items = Arguments.createArray()
|
|
969
|
+
uris.forEachIndexed { i, u ->
|
|
970
|
+
val it0 = if (isVideoUri(u)) {
|
|
971
|
+
val st = lastVideoEditStates[u.toString()] ?: com.rnattachmentkit.VideoEditState(qualityHeight = videoOptions.defaultQuality)
|
|
972
|
+
com.rnattachmentkit.MediaProcessing.videoItem(reactApplicationContext, i, u, st, videoOptions)
|
|
973
|
+
} else {
|
|
974
|
+
com.rnattachmentkit.MediaProcessing.imageItem(reactApplicationContext, i, u, maxWidth, maxHeight)
|
|
975
|
+
}
|
|
976
|
+
items.pushMap(Arguments.createMap().apply {
|
|
977
|
+
putInt("index", it0.index)
|
|
978
|
+
putString("mediaType", if (it0.isVideo) "video" else "image")
|
|
979
|
+
putString("originalUri", it0.originalUri)
|
|
980
|
+
putInt("width", it0.width)
|
|
981
|
+
putInt("height", it0.height)
|
|
982
|
+
it0.durationSec?.let { d -> putDouble("duration", d) }
|
|
983
|
+
it0.thumbnailUri?.let { t ->
|
|
984
|
+
putString("thumbnailUri", t)
|
|
985
|
+
putInt("thumbnailWidth", it0.thumbnailWidth)
|
|
986
|
+
putInt("thumbnailHeight", it0.thumbnailHeight)
|
|
987
|
+
}
|
|
988
|
+
})
|
|
989
|
+
}
|
|
990
|
+
emitGalleryEvent("onProcessingStart", Arguments.createMap().apply {
|
|
991
|
+
putString("processingId", processingId)
|
|
992
|
+
putInt("total", totalCount)
|
|
993
|
+
putArray("items", items)
|
|
994
|
+
})
|
|
995
|
+
} catch (e: Exception) {
|
|
996
|
+
android.util.Log.w("RNAttachmentKit", "onProcessingStart failed: ${e.message}")
|
|
997
|
+
}
|
|
998
|
+
var hud: com.rnattachmentkit.VideoExportHUD? = null
|
|
999
|
+
if (hasVideo && showVideoHUD) {
|
|
1000
|
+
withContext(Dispatchers.Main) {
|
|
1001
|
+
val act = getCurrentActivity() ?: return@withContext
|
|
1002
|
+
hud = com.rnattachmentkit.VideoExportHUD(act, lastThemeColorInt, lastLanguageCode).also { h ->
|
|
1003
|
+
h.onCancel = {
|
|
1004
|
+
videoCancelled = true
|
|
1005
|
+
currentVideoExporter?.cancel()
|
|
1006
|
+
}
|
|
1007
|
+
h.show()
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
val videoFiles = mutableListOf<File>()
|
|
1012
|
+
|
|
1013
|
+
try {
|
|
867
1014
|
for (uri in uris) {
|
|
1015
|
+
if (videoCancelled) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1016
|
+
if (isVideoUri(uri)) {
|
|
1017
|
+
val idx = currentIndex
|
|
1018
|
+
val exporter = com.rnattachmentkit.VideoExporter(reactApplicationContext)
|
|
1019
|
+
currentVideoExporter = exporter
|
|
1020
|
+
val cancelledMap = {
|
|
1021
|
+
WritableNativeMap().apply {
|
|
1022
|
+
putString("originalUri", uri.toString())
|
|
1023
|
+
putString("mediaType", "video")
|
|
1024
|
+
putString("error", "CANCELLED")
|
|
1025
|
+
putBoolean("cancelled", true)
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
val resultMap = try {
|
|
1029
|
+
// 앱이 이미 취소한 동영상은 건너뜀
|
|
1030
|
+
if (job.isCancelled(idx)) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1031
|
+
job.begin(idx, exporter)
|
|
1032
|
+
val state = lastVideoEditStates[uri.toString()] ?: com.rnattachmentkit.VideoEditState(qualityHeight = videoOptions.defaultQuality)
|
|
1033
|
+
val throttle = com.rnattachmentkit.ProgressThrottle()
|
|
1034
|
+
val onProgress: (Float) -> Unit = { p ->
|
|
1035
|
+
hud?.let { h -> android.os.Handler(android.os.Looper.getMainLooper()).post { h.update(p, idx, totalCount) } }
|
|
1036
|
+
if (throttle.shouldSend(p)) emitGalleryEvent("onVideoProgress", Arguments.createMap().apply {
|
|
1037
|
+
putInt("index", idx); putInt("total", totalCount); putDouble("progress", p.toDouble())
|
|
1038
|
+
putString("processingId", processingId)
|
|
1039
|
+
})
|
|
1040
|
+
}
|
|
1041
|
+
// 편집 화면에서 미리 만들어 둔 파일이 있으면(또는 만드는 중이면 끝나고) 그대로 씀
|
|
1042
|
+
// 기다리는 동안 앱(X)이나 HUD 가 취소하면 바로 멈춤 (전엔 미리 만들기가 끝날 때까지 못 빠져나왔음)
|
|
1043
|
+
val stop = { videoCancelled || job.isCancelled(idx) }
|
|
1044
|
+
val res = com.rnattachmentkit.VideoPrepareManager.takeReady(uri, state, onProgress, stop)
|
|
1045
|
+
?: run {
|
|
1046
|
+
if (stop()) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1047
|
+
exporter.export(uri, state, videoOptions, reactApplicationContext.cacheDir, "turbo_video_${System.currentTimeMillis()}_$idx", onProgress)
|
|
1048
|
+
}
|
|
1049
|
+
withContext(Dispatchers.Main) { hud?.update(1f, idx, totalCount) }
|
|
1050
|
+
// 미리 만든 파일을 기다리는 동안 취소됐으면 결과를 쓰지 않음
|
|
1051
|
+
if (job.isCancelled(idx)) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1052
|
+
videoFiles += res.file
|
|
1053
|
+
res.thumbnailFile?.let { videoFiles += it }
|
|
1054
|
+
WritableNativeMap().apply {
|
|
1055
|
+
putString("uri", Uri.fromFile(res.file).toString())
|
|
1056
|
+
putString("originalUri", uri.toString())
|
|
1057
|
+
putString("mediaType", "video")
|
|
1058
|
+
putString("type", "video/mp4")
|
|
1059
|
+
putInt("width", res.width)
|
|
1060
|
+
putInt("height", res.height)
|
|
1061
|
+
putDouble("duration", res.durationMs / 1000.0)
|
|
1062
|
+
putDouble("fileSize", res.fileSize.toDouble())
|
|
1063
|
+
putString("fileName", res.file.name)
|
|
1064
|
+
putString("fileExtension", "mp4")
|
|
1065
|
+
putString("videoCodec", res.codec)
|
|
1066
|
+
putInt("videoBitrate", res.bitrate)
|
|
1067
|
+
putBoolean("edited", res.edited)
|
|
1068
|
+
if (res.thumbnailFile != null) {
|
|
1069
|
+
putString("thumbnailUri", Uri.fromFile(res.thumbnailFile).toString())
|
|
1070
|
+
putInt("thumbnailWidth", res.thumbnailWidth)
|
|
1071
|
+
putInt("thumbnailHeight", res.thumbnailHeight)
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
1075
|
+
// 앱이 이 동영상만 취소 → 나머지는 계속. HUD 취소면 전체 취소
|
|
1076
|
+
if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else throw e
|
|
1077
|
+
} catch (e: Exception) {
|
|
1078
|
+
// 취소하면서 변환기가 다른 예외로 끝날 수도 있음 → 취소로 처리
|
|
1079
|
+
if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else {
|
|
1080
|
+
android.util.Log.w("RNAttachmentKit", "video export failed: ${e.message}", e)
|
|
1081
|
+
WritableNativeMap().apply {
|
|
1082
|
+
putString("originalUri", uri.toString())
|
|
1083
|
+
putString("mediaType", "video")
|
|
1084
|
+
putString("error", e.message ?: "export failed")
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
} finally {
|
|
1088
|
+
currentVideoExporter = null
|
|
1089
|
+
job.end()
|
|
1090
|
+
}
|
|
1091
|
+
results.add(resultMap)
|
|
1092
|
+
try {
|
|
1093
|
+
val copy = WritableNativeMap().apply { merge(resultMap) }
|
|
1094
|
+
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1095
|
+
.emit("onImageProcessed", WritableNativeMap().apply {
|
|
1096
|
+
putInt("total", totalCount); putInt("index", idx); putMap("image", copy)
|
|
1097
|
+
putString("processingId", processingId)
|
|
1098
|
+
})
|
|
1099
|
+
} catch (_: Exception) {}
|
|
1100
|
+
currentIndex++
|
|
1101
|
+
continue
|
|
1102
|
+
}
|
|
868
1103
|
try {
|
|
869
1104
|
// 1. Get EXIF Orientation
|
|
870
1105
|
var orientation = android.media.ExifInterface.ORIENTATION_NORMAL
|
|
@@ -995,6 +1230,7 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
995
1230
|
val eventMap = WritableNativeMap().apply {
|
|
996
1231
|
putInt("total", totalCount)
|
|
997
1232
|
putInt("index", currentIndex)
|
|
1233
|
+
putString("processingId", processingId)
|
|
998
1234
|
putMap("image", createImageInfo())
|
|
999
1235
|
}
|
|
1000
1236
|
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
@@ -1010,6 +1246,14 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1010
1246
|
continue
|
|
1011
1247
|
}
|
|
1012
1248
|
}
|
|
1249
|
+
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
1250
|
+
// 취소: 지금까지 만든 동영상 파일은 지움
|
|
1251
|
+
videoFiles.forEach { try { it.delete() } catch (_: Exception) {} }
|
|
1252
|
+
throw e
|
|
1253
|
+
} finally {
|
|
1254
|
+
activeProcessingJobs.remove(processingId)
|
|
1255
|
+
if (hud != null) withContext(Dispatchers.Main) { hud?.dismiss() }
|
|
1256
|
+
}
|
|
1013
1257
|
|
|
1014
1258
|
results
|
|
1015
1259
|
}
|
|
@@ -1289,6 +1533,13 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1289
1533
|
)
|
|
1290
1534
|
if (rect != null) f.sourceRectProvider = { mapViewerSourceRect }
|
|
1291
1535
|
var settled = false
|
|
1536
|
+
f.onWillClose = { ms ->
|
|
1537
|
+
try {
|
|
1538
|
+
reactApplicationContext
|
|
1539
|
+
.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1540
|
+
.emit("onMapViewerWillClose", com.facebook.react.bridge.Arguments.createMap().apply { putInt("durationMs", ms) })
|
|
1541
|
+
} catch (_: Exception) {}
|
|
1542
|
+
}
|
|
1292
1543
|
f.onClosed = {
|
|
1293
1544
|
if (!settled) { settled = true; promise.resolve(null) }
|
|
1294
1545
|
mapViewerSourceRect = null
|
|
@@ -1417,6 +1668,24 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1417
1668
|
}
|
|
1418
1669
|
}
|
|
1419
1670
|
|
|
1671
|
+
// ── AI 기능 설정·잔액 (넘긴 값만 바뀜). 사용·충전 요청은 onAIUsage / onAIChargeRequired 이벤트로 알림 ──
|
|
1672
|
+
|
|
1673
|
+
@ReactMethod
|
|
1674
|
+
fun setAIConfig(config: ReadableMap, promise: Promise) {
|
|
1675
|
+
AIBilling.apply(config.toHashMap())
|
|
1676
|
+
val ctx = reactApplicationContext
|
|
1677
|
+
fun emit(name: String, body: Map<String, Any>) {
|
|
1678
|
+
// 앱이 내려가는 중이면 조용히 무시
|
|
1679
|
+
runCatching {
|
|
1680
|
+
ctx.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1681
|
+
.emit(name, com.facebook.react.bridge.Arguments.makeNativeMap(body))
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
AIBilling.onUsage = { emit("onAIUsage", it) }
|
|
1685
|
+
AIBilling.onChargeRequired = { emit("onAIChargeRequired", it) }
|
|
1686
|
+
promise.resolve(null)
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1420
1689
|
// ── 역지오코딩 (기기 Geocoder, 디스크 캐시) ──
|
|
1421
1690
|
|
|
1422
1691
|
@ReactMethod
|
package/ios/RNAttachmentKit.m
CHANGED
|
@@ -32,6 +32,11 @@ RCT_EXTERN_METHOD(openViewer:(NSDictionary *)options
|
|
|
32
32
|
RCT_EXTERN_METHOD(closeGallery:(RCTPromiseResolveBlock)resolve
|
|
33
33
|
reject:(RCTPromiseRejectBlock)reject)
|
|
34
34
|
|
|
35
|
+
RCT_EXTERN_METHOD(cancelVideoProcessing:(nonnull NSNumber *)index
|
|
36
|
+
processingId:(NSString *)processingId
|
|
37
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
38
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
39
|
+
|
|
35
40
|
RCT_EXTERN_METHOD(injectImageCache:(NSString *)urlString
|
|
36
41
|
localPath:(NSString *)localPath
|
|
37
42
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
@@ -57,6 +62,10 @@ RCT_EXTERN_METHOD(reverseGeocode:(NSDictionary *)options
|
|
|
57
62
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
58
63
|
reject:(RCTPromiseRejectBlock)reject)
|
|
59
64
|
|
|
65
|
+
RCT_EXTERN_METHOD(setAIConfig:(NSDictionary *)config
|
|
66
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
67
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
68
|
+
|
|
60
69
|
RCT_EXTERN_METHOD(clearReverseGeocodeCache:(RCTPromiseResolveBlock)resolve
|
|
61
70
|
reject:(RCTPromiseRejectBlock)reject)
|
|
62
71
|
|
|
@@ -114,6 +123,11 @@ RCT_EXTERN_METHOD(openViewer:(NSDictionary *)options
|
|
|
114
123
|
RCT_EXTERN_METHOD(closeGallery:(RCTPromiseResolveBlock)resolve
|
|
115
124
|
reject:(RCTPromiseRejectBlock)reject)
|
|
116
125
|
|
|
126
|
+
RCT_EXTERN_METHOD(cancelVideoProcessing:(nonnull NSNumber *)index
|
|
127
|
+
processingId:(NSString *)processingId
|
|
128
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
129
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
130
|
+
|
|
117
131
|
RCT_EXTERN_METHOD(injectImageCache:(NSString *)urlString
|
|
118
132
|
localPath:(NSString *)localPath
|
|
119
133
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
@@ -139,6 +153,10 @@ RCT_EXTERN_METHOD(reverseGeocode:(NSDictionary *)options
|
|
|
139
153
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
140
154
|
reject:(RCTPromiseRejectBlock)reject)
|
|
141
155
|
|
|
156
|
+
RCT_EXTERN_METHOD(setAIConfig:(NSDictionary *)config
|
|
157
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
158
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
159
|
+
|
|
142
160
|
RCT_EXTERN_METHOD(clearReverseGeocodeCache:(RCTPromiseResolveBlock)resolve
|
|
143
161
|
reject:(RCTPromiseRejectBlock)reject)
|
|
144
162
|
|