react-native-attachment-kit 1.0.2 → 1.0.6
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 +121 -0
- package/README.md +446 -206
- package/android/build.gradle +17 -0
- package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
- package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +587 -22
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +592 -21
- package/ios/RNAttachmentKit.m +62 -0
- package/ios/RNAttachmentKit.swift +725 -29
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Headers/RNAttachmentKit-Swift.h +51 -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 +51876 -18835
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +310 -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 +310 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_reflect~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_reflect~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_back~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_back~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_blur~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_blur~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check_small~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check_small~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_close~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_close~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_text_add~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/ToolbarIcons/ic_text_add~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +195 -63
- 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 +102 -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 +51876 -18835
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +310 -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 +310 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +51876 -18835
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +310 -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 +310 -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/ToolbarIcons/ic_crop_reflect~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_crop_reflect~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_crop_rotate~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_delete~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_forward~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_drawing_reward~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_back~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_back~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_blur~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_blur~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check_small~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check_small~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_check~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_close~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_close~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_crop~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_drawing~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_effect~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_emoji~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_eraser~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_mosaic~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_editor_text~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_text_add~dark.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/ToolbarIcons/ic_text_add~light.webp +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +205 -73
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_reflect~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_reflect~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_rotate~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_crop_rotate~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_delete~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_delete~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_forward~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_forward~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_reward~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_drawing_reward~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_back~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_back~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_blur~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_blur~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_check_small~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_check_small~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_check~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_check~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_close~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_close~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_crop~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_crop~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_drawing~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_drawing~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_effect~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_effect~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_emoji~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_emoji~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_eraser~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_eraser~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_mosaic~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_mosaic~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_text~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_editor_text~light.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_text_add~dark.webp +0 -0
- package/ios/RNAttachmentKitAssets/ToolbarIcons/ic_text_add~light.webp +0 -0
- package/package.json +1 -1
- package/react-native-attachment-kit.podspec +1 -1
- package/src/NativeAttachmentKit.ts +15 -0
- package/src/index.d.ts +5 -56
- package/src/index.ts +301 -37
- package/src/types.ts +380 -11
|
@@ -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")
|
|
@@ -262,31 +305,107 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
262
305
|
}
|
|
263
306
|
|
|
264
307
|
pickImagesPromise = promise
|
|
308
|
+
// 이번 호출의 promise 를 붙잡아 둠 — 리스너가 모듈 필드를 읽으면, 시트가 열린 채 openGallery 를 다시 불렀을 때
|
|
309
|
+
// 이전 시트가 치워지며 오는 onDismiss 가 "새" promise 를 거부해 버림. 시트마다 자기 promise 를 한 번만 끝냄
|
|
310
|
+
val thisPromise = promise
|
|
311
|
+
var settled = false
|
|
312
|
+
val settle: ((Promise) -> Unit) -> Unit = { block ->
|
|
313
|
+
if (!settled) {
|
|
314
|
+
settled = true
|
|
315
|
+
if (pickImagesPromise === thisPromise) pickImagesPromise = null
|
|
316
|
+
block(thisPromise)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// 지난 호출이 남긴 오래된 임시 파일(24시간 지난 것) 정리 — 백그라운드
|
|
320
|
+
moduleScope.launch(Dispatchers.IO) {
|
|
321
|
+
try { TempFileCleaner.cleanStale(reactApplicationContext) } catch (_: Exception) {}
|
|
322
|
+
}
|
|
265
323
|
|
|
266
324
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
267
325
|
Manifest.permission.READ_MEDIA_IMAGES
|
|
268
326
|
} else {
|
|
269
327
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
270
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
|
+
}
|
|
271
335
|
|
|
272
336
|
val showSheet = {
|
|
273
337
|
fragmentActivity.runOnUiThread {
|
|
274
338
|
try {
|
|
275
339
|
val bottomSheet = ImagePickerBottomSheet.newInstance(
|
|
276
340
|
maxSelection, languageCode, enableEditor, profileMode, maxWidth, maxHeight, themeColor,
|
|
277
|
-
forceMultiMode = forceMultiMode
|
|
341
|
+
forceMultiMode = forceMultiMode,
|
|
342
|
+
// 첨부 메뉴(갤러리 / 위치 / 파일). 기본 false — 켜지 않으면 기존과 같음
|
|
343
|
+
showAttachMenu = options.hasKey("showAttachMenu") && options.getBoolean("showAttachMenu"),
|
|
344
|
+
// (Android) 첨부 메뉴 바 아래 여백 (dp). 없으면 기본 12 / 8
|
|
345
|
+
attachMenuSinkIntoNav = if (options.hasKey("attachMenuSinkIntoNav") && !options.isNull("attachMenuSinkIntoNav"))
|
|
346
|
+
options.getDouble("attachMenuSinkIntoNav").toFloat() else com.rnattachmentkit.AttachMenuBarView.SINK_INTO_NAV_DP.toFloat(),
|
|
347
|
+
attachMenuMinBottomMargin = if (options.hasKey("attachMenuMinBottomMargin") && !options.isNull("attachMenuMinBottomMargin"))
|
|
348
|
+
options.getDouble("attachMenuMinBottomMargin").toFloat() else com.rnattachmentkit.AttachMenuBarView.BOTTOM_MARGIN_DP.toFloat(),
|
|
349
|
+
// 위치 보내기 때 지도 이미지 캡처 여부. 기본 true (기존 그대로). false 면 이미지 없이 위치 정보만
|
|
350
|
+
captureLocationImage = !(options.hasKey("captureLocationImage") && !options.isNull("captureLocationImage")) ||
|
|
351
|
+
options.getBoolean("captureLocationImage"),
|
|
352
|
+
// 동영상 (mediaType "video"/"all" 일 때 그리드에 동영상, 길이 제한)
|
|
353
|
+
mediaType = mediaType,
|
|
354
|
+
maxVideoDurationSec = videoOptions.maxDurationSec
|
|
278
355
|
)
|
|
356
|
+
// 위치 탭에서 보내기 → onLocationSelected 이벤트로 결과를 넘기고, openGallery 는 빈 배열로 끝냄
|
|
357
|
+
bottomSheet.setOnLocationSelectedListener { loc ->
|
|
358
|
+
val body = com.facebook.react.bridge.Arguments.createMap()
|
|
359
|
+
for ((k, v) in loc) {
|
|
360
|
+
when (v) {
|
|
361
|
+
is Int -> body.putInt(k, v)
|
|
362
|
+
is Long -> body.putDouble(k, v.toDouble())
|
|
363
|
+
is Double -> body.putDouble(k, v)
|
|
364
|
+
is Float -> body.putDouble(k, v.toDouble())
|
|
365
|
+
is Boolean -> body.putBoolean(k, v)
|
|
366
|
+
is String -> body.putString(k, v)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
emitGalleryEvent("onLocationSelected", body)
|
|
370
|
+
settle { it.resolve(com.facebook.react.bridge.Arguments.createArray()) }
|
|
371
|
+
}
|
|
372
|
+
// 파일 탭에서 보내기 → onFilesSelected 이벤트 { files: [{uri, path, name, size, mimeType}], failed: [{name, error}] }, openGallery 는 빈 배열
|
|
373
|
+
bottomSheet.setOnFilesResultListener { files, failed ->
|
|
374
|
+
val arr = com.facebook.react.bridge.Arguments.createArray()
|
|
375
|
+
for (file in files) {
|
|
376
|
+
val m = com.facebook.react.bridge.Arguments.createMap()
|
|
377
|
+
for ((k, v) in file) {
|
|
378
|
+
when (v) {
|
|
379
|
+
is Double -> m.putDouble(k, v)
|
|
380
|
+
is String -> m.putString(k, v)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
arr.pushMap(m)
|
|
384
|
+
}
|
|
385
|
+
val body = com.facebook.react.bridge.Arguments.createMap()
|
|
386
|
+
body.putArray("files", arr)
|
|
387
|
+
// 복사에 실패한 파일 (추가 필드 — files 는 예전과 같음)
|
|
388
|
+
val failedArr = com.facebook.react.bridge.Arguments.createArray()
|
|
389
|
+
for (f in failed) {
|
|
390
|
+
val m = com.facebook.react.bridge.Arguments.createMap()
|
|
391
|
+
m.putString("name", f["name"]?.toString() ?: "")
|
|
392
|
+
m.putString("error", f["error"]?.toString() ?: "")
|
|
393
|
+
failedArr.pushMap(m)
|
|
394
|
+
}
|
|
395
|
+
body.putArray("failed", failedArr)
|
|
396
|
+
emitGalleryEvent("onFilesSelected", body)
|
|
397
|
+
settle { it.resolve(com.facebook.react.bridge.Arguments.createArray()) }
|
|
398
|
+
}
|
|
279
399
|
bottomSheet.setOnImagesSelectedListener { uris ->
|
|
400
|
+
lastVideoEditStates = HashMap(bottomSheet.videoEditStates)
|
|
280
401
|
val result = WritableNativeArray()
|
|
281
402
|
for (uri in uris) {
|
|
282
403
|
result.pushString(uri.toString())
|
|
283
404
|
}
|
|
284
|
-
handleSelectedImages(result,
|
|
285
|
-
pickImagesPromise = null
|
|
405
|
+
settle { handleSelectedImages(result, it) }
|
|
286
406
|
}
|
|
287
407
|
bottomSheet.setOnCancelledListener {
|
|
288
|
-
|
|
289
|
-
pickImagesPromise = null
|
|
408
|
+
settle { it.reject(E_PICKER_CANCELLED, "User cancelled image picker") }
|
|
290
409
|
}
|
|
291
410
|
|
|
292
411
|
val existingFragment = fragmentActivity.supportFragmentManager.findFragmentByTag("ImagePickerBottomSheet")
|
|
@@ -299,13 +418,12 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
299
418
|
.commitAllowingStateLoss()
|
|
300
419
|
|
|
301
420
|
} catch (e: Exception) {
|
|
302
|
-
|
|
303
|
-
pickImagesPromise = null
|
|
421
|
+
settle { it.reject(E_FAILED_TO_PICK, "Failed to open image picker: ${e.message}", e) }
|
|
304
422
|
}
|
|
305
423
|
}
|
|
306
424
|
}
|
|
307
425
|
|
|
308
|
-
if (ContextCompat.checkSelfPermission(fragmentActivity,
|
|
426
|
+
if (mediaPermissions.all { ContextCompat.checkSelfPermission(fragmentActivity, it) == PackageManager.PERMISSION_GRANTED }) {
|
|
309
427
|
showSheet()
|
|
310
428
|
} else {
|
|
311
429
|
val permissionAwareActivity = fragmentActivity as? PermissionAwareActivity
|
|
@@ -320,28 +438,25 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
320
438
|
showSheet()
|
|
321
439
|
return
|
|
322
440
|
}
|
|
323
|
-
|
|
441
|
+
mediaPermissions + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
|
|
324
442
|
} else {
|
|
325
|
-
|
|
443
|
+
mediaPermissions
|
|
326
444
|
}
|
|
327
445
|
|
|
328
446
|
permissionAwareActivity.requestPermissions(permissionsToRequest, 100, object : PermissionListener {
|
|
329
447
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray): Boolean {
|
|
330
448
|
if (requestCode == 100 && grantResults != null) {
|
|
331
449
|
val isGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
(grantResults.size > 1 && grantResults[1] == PackageManager.PERMISSION_GRANTED)
|
|
335
|
-
)
|
|
450
|
+
// 14+: 전체 허용이든 "일부 선택"이든 하나라도 허용이면 진행
|
|
451
|
+
grantResults.any { it == PackageManager.PERMISSION_GRANTED }
|
|
336
452
|
} else {
|
|
337
|
-
grantResults.isNotEmpty() && grantResults
|
|
453
|
+
grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
|
338
454
|
}
|
|
339
455
|
|
|
340
456
|
if (isGranted) {
|
|
341
457
|
showSheet()
|
|
342
458
|
} else {
|
|
343
|
-
|
|
344
|
-
pickImagesPromise = null
|
|
459
|
+
settle { it.reject("E_PERMISSION_DENIED", "Permission denied") }
|
|
345
460
|
}
|
|
346
461
|
}
|
|
347
462
|
return true
|
|
@@ -398,6 +513,15 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
398
513
|
promise.resolve(null)
|
|
399
514
|
}
|
|
400
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
|
+
|
|
401
525
|
@ReactMethod
|
|
402
526
|
override fun closeGallery(promise: Promise) {
|
|
403
527
|
val currentActivity = currentActivity
|
|
@@ -536,6 +660,22 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
536
660
|
putStringArrayList("placeholderImages", placeholders)
|
|
537
661
|
}
|
|
538
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
|
+
}
|
|
539
679
|
// 상단바 저장/공유/삭제 버튼과 그 문구들 (iOS와 동일한 옵션 키). 문구를 안 넘기면
|
|
540
680
|
// 네이티브 쪽 로케일 리소스(values-ko/ja/zh, 그 외 en)가 자동 적용됩니다.
|
|
541
681
|
putBoolean("showActionButtons", !options.hasKey("showActionButtons") || options.getBoolean("showActionButtons"))
|
|
@@ -728,9 +868,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
728
868
|
return
|
|
729
869
|
}
|
|
730
870
|
|
|
871
|
+
val snap = sendSnapshot()
|
|
731
872
|
moduleScope.launch {
|
|
732
873
|
try {
|
|
733
|
-
val results = processImages(uris)
|
|
874
|
+
val results = processImages(uris, snap)
|
|
734
875
|
if (results.isNotEmpty()) {
|
|
735
876
|
promise.resolve(results[0])
|
|
736
877
|
} else {
|
|
@@ -774,7 +915,7 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
774
915
|
// Process images asynchronously
|
|
775
916
|
val eventMap = com.facebook.react.bridge.WritableNativeMap()
|
|
776
917
|
eventMap.putInt("selectedCount", uris.size)
|
|
777
|
-
eventMap.putInt("maxSelection",
|
|
918
|
+
eventMap.putInt("maxSelection", lastMaxSelection)
|
|
778
919
|
try {
|
|
779
920
|
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
780
921
|
.emit("onSelectionChange", eventMap)
|
|
@@ -782,25 +923,183 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
782
923
|
// Ignore
|
|
783
924
|
}
|
|
784
925
|
|
|
926
|
+
val snap = sendSnapshot()
|
|
785
927
|
moduleScope.launch {
|
|
786
928
|
try {
|
|
787
|
-
val results = processImages(uris)
|
|
929
|
+
val results = processImages(uris, snap)
|
|
788
930
|
val array = com.facebook.react.bridge.Arguments.createArray()
|
|
789
931
|
results.forEach { array.pushMap(it) }
|
|
790
932
|
actualPromise.resolve(array)
|
|
933
|
+
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
934
|
+
actualPromise.reject("CANCELLED", "Video export cancelled")
|
|
791
935
|
} catch (e: Exception) {
|
|
792
936
|
actualPromise.reject(E_FAILED_TO_PICK, "Failed to process images: ${e.message}", e)
|
|
793
937
|
}
|
|
794
938
|
}
|
|
795
939
|
}
|
|
796
940
|
|
|
797
|
-
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()
|
|
798
952
|
val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
|
|
799
953
|
val contentResolver = reactApplicationContext.contentResolver
|
|
800
954
|
val totalCount = uris.size
|
|
801
955
|
var currentIndex = 0
|
|
802
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 {
|
|
803
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
|
+
}
|
|
804
1103
|
try {
|
|
805
1104
|
// 1. Get EXIF Orientation
|
|
806
1105
|
var orientation = android.media.ExifInterface.ORIENTATION_NORMAL
|
|
@@ -931,6 +1230,7 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
931
1230
|
val eventMap = WritableNativeMap().apply {
|
|
932
1231
|
putInt("total", totalCount)
|
|
933
1232
|
putInt("index", currentIndex)
|
|
1233
|
+
putString("processingId", processingId)
|
|
934
1234
|
putMap("image", createImageInfo())
|
|
935
1235
|
}
|
|
936
1236
|
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
@@ -946,6 +1246,14 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
946
1246
|
continue
|
|
947
1247
|
}
|
|
948
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
|
+
}
|
|
949
1257
|
|
|
950
1258
|
results
|
|
951
1259
|
}
|
|
@@ -1142,4 +1450,267 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1142
1450
|
.unregisterReceiver(imageDeletedReceiver)
|
|
1143
1451
|
}
|
|
1144
1452
|
|
|
1453
|
+
// ── 지도 뷰어 (위치 메시지 → 지도) ──
|
|
1454
|
+
|
|
1455
|
+
private var mapViewer: java.lang.ref.WeakReference<MapViewerDialogFragment>? = null
|
|
1456
|
+
/** updateMapViewerSourceRect 로 갱신되는 닫힘 위치 (화면 px) */
|
|
1457
|
+
@Volatile private var mapViewerSourceRect: android.graphics.Rect? = null
|
|
1458
|
+
|
|
1459
|
+
/** RN measureInWindow 값(dp, 창 기준) → 화면 px Rect */
|
|
1460
|
+
private fun mapRectFromDp(map: ReadableMap?, activity: Activity): android.graphics.Rect? {
|
|
1461
|
+
if (map == null || !map.hasKey("x") || !map.hasKey("y") || !map.hasKey("width") || !map.hasKey("height")) return null
|
|
1462
|
+
val w = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(map.getDouble("width").toFloat())
|
|
1463
|
+
val h = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(map.getDouble("height").toFloat())
|
|
1464
|
+
if (w <= 0f || h <= 0f) return null
|
|
1465
|
+
val loc = IntArray(2)
|
|
1466
|
+
try { activity.window?.decorView?.getLocationOnScreen(loc) } catch (_: Exception) {}
|
|
1467
|
+
val x = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(map.getDouble("x").toFloat()) + loc[0]
|
|
1468
|
+
val y = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(map.getDouble("y").toFloat()) + loc[1]
|
|
1469
|
+
return android.graphics.Rect(Math.round(x), Math.round(y), Math.round(x + w), Math.round(y + h))
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/** 말풍선 이미지 → 로컬 파일 경로. file:// 는 그대로, http(s) 는 Glide 캐시 파일. 실패·지연 시 null (이미지 없이 엶) */
|
|
1473
|
+
private fun mapSourceImagePath(uri: String?): String? {
|
|
1474
|
+
if (uri.isNullOrBlank()) return null
|
|
1475
|
+
if (uri.startsWith("/")) return uri.takeIf { File(it).exists() }
|
|
1476
|
+
if (uri.startsWith("file://")) return Uri.parse(uri).path?.takeIf { File(it).exists() }
|
|
1477
|
+
if (!uri.startsWith("http://") && !uri.startsWith("https://")) return null
|
|
1478
|
+
return try {
|
|
1479
|
+
com.bumptech.glide.Glide.with(reactApplicationContext.applicationContext)
|
|
1480
|
+
.downloadOnly().load(uri).submit()
|
|
1481
|
+
.get(1500, java.util.concurrent.TimeUnit.MILLISECONDS)
|
|
1482
|
+
?.absolutePath
|
|
1483
|
+
} catch (e: Exception) {
|
|
1484
|
+
android.util.Log.w("RNAttachmentKit", "openMapViewer: source image not loaded (${e.javaClass.simpleName})")
|
|
1485
|
+
null
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
@ReactMethod
|
|
1490
|
+
fun openMapViewer(options: ReadableMap, promise: Promise) {
|
|
1491
|
+
val activity = getCurrentActivity() as? FragmentActivity
|
|
1492
|
+
if (activity == null) {
|
|
1493
|
+
promise.reject("NO_ACTIVITY", "Activity is not a FragmentActivity")
|
|
1494
|
+
return
|
|
1495
|
+
}
|
|
1496
|
+
if (!options.hasKey("latitude") || !options.hasKey("longitude")) {
|
|
1497
|
+
promise.reject("ERROR", "latitude and longitude are required")
|
|
1498
|
+
return
|
|
1499
|
+
}
|
|
1500
|
+
fun str(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getString(key) else null
|
|
1501
|
+
fun num(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getDouble(key) else null
|
|
1502
|
+
val lat = options.getDouble("latitude")
|
|
1503
|
+
val lng = options.getDouble("longitude")
|
|
1504
|
+
val rect = mapRectFromDp(if (options.hasKey("sourceRect") && !options.isNull("sourceRect")) options.getMap("sourceRect") else null, activity)
|
|
1505
|
+
val imageUri = str("sourceImageUri")
|
|
1506
|
+
val themeColor = str("themeColor")?.let { try { android.graphics.Color.parseColor(it) } catch (_: Exception) { null } }
|
|
1507
|
+
val radiusPx = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP((num("sourceCornerRadius") ?: 12.0).toFloat())
|
|
1508
|
+
val showDirections = if (options.hasKey("showDirections") && !options.isNull("showDirections")) options.getBoolean("showDirections") else true
|
|
1509
|
+
|
|
1510
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
1511
|
+
val imagePath = if (rect != null) mapSourceImagePath(imageUri) else null
|
|
1512
|
+
withContext(Dispatchers.Main) {
|
|
1513
|
+
if (activity.isFinishing || activity.isDestroyed || activity.supportFragmentManager.isStateSaved) {
|
|
1514
|
+
promise.reject("ERROR", "Activity is not available")
|
|
1515
|
+
return@withContext
|
|
1516
|
+
}
|
|
1517
|
+
try {
|
|
1518
|
+
mapViewerSourceRect = rect
|
|
1519
|
+
val f = MapViewerDialogFragment.show(
|
|
1520
|
+
activity, lat, lng,
|
|
1521
|
+
title = str("title"),
|
|
1522
|
+
address = str("address"),
|
|
1523
|
+
markerType = str("markerType"),
|
|
1524
|
+
themeColor = themeColor,
|
|
1525
|
+
languageCode = str("languageCode") ?: "en",
|
|
1526
|
+
showDirections = showDirections,
|
|
1527
|
+
sourceRect = rect,
|
|
1528
|
+
sourceImagePath = imagePath,
|
|
1529
|
+
sourceCornerRadius = radiusPx,
|
|
1530
|
+
spanMeters = num("spanMeters") ?: 0.0,
|
|
1531
|
+
appearance = str("appearance"),
|
|
1532
|
+
addressLanguageCode = str("addressLanguageCode"),
|
|
1533
|
+
)
|
|
1534
|
+
if (rect != null) f.sourceRectProvider = { mapViewerSourceRect }
|
|
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
|
+
}
|
|
1543
|
+
f.onClosed = {
|
|
1544
|
+
if (!settled) { settled = true; promise.resolve(null) }
|
|
1545
|
+
mapViewerSourceRect = null
|
|
1546
|
+
mapViewer = null
|
|
1547
|
+
}
|
|
1548
|
+
mapViewer = java.lang.ref.WeakReference(f)
|
|
1549
|
+
} catch (e: Exception) {
|
|
1550
|
+
promise.reject("ERROR", e.message, e)
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
@ReactMethod
|
|
1557
|
+
fun updateMapViewerSourceRect(rect: ReadableMap, promise: Promise) {
|
|
1558
|
+
val activity = getCurrentActivity()
|
|
1559
|
+
if (mapViewer?.get() != null && activity != null) mapViewerSourceRect = mapRectFromDp(rect, activity)
|
|
1560
|
+
promise.resolve(null)
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* 위치 메시지 지도 썸네일을 이 기기에서 그림 (테마별 캐시). 결과 { uri, path, width, height(px), cached }
|
|
1565
|
+
* options: latitude, longitude, width, height (dp, 필수) / markerType, placeCategory, spanMeters, themeColor,
|
|
1566
|
+
* appearance ("light"/"dark"/"system"), myLatitude + myLongitude (둘 다 있을 때만)
|
|
1567
|
+
* aspect ("square" | "portrait" | "device"): 주면 width 만 필수, height 는 비율로 (square = 1:1,
|
|
1568
|
+
* portrait = 480:1040 보내던 이미지와 같은 9:19.5, device = 실제 화면 긴 변/짧은 변). 모르는 값이면 없는 것과 같음
|
|
1569
|
+
* maxWidth / maxHeight (px, 기본 512, 0 이하면 무시): 출력 WebP 가 이 안에 들어가게 비율대로 줄임
|
|
1570
|
+
* (k = min(density, maxWidth / widthDp, maxHeight / heightDp)) → 결과 width/height 는 출력 px
|
|
1571
|
+
*/
|
|
1572
|
+
@ReactMethod
|
|
1573
|
+
fun renderLocationThumbnail(options: ReadableMap, promise: Promise) {
|
|
1574
|
+
fun str(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getString(key) else null
|
|
1575
|
+
fun num(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getDouble(key) else null
|
|
1576
|
+
val lat = num("latitude")
|
|
1577
|
+
val lng = num("longitude")
|
|
1578
|
+
val wDp = num("width")
|
|
1579
|
+
val activity = getCurrentActivity()
|
|
1580
|
+
val aspectRatio: Double? = when (str("aspect")?.lowercase()) {
|
|
1581
|
+
"square" -> 1.0
|
|
1582
|
+
"portrait" -> 1040.0 / 480.0
|
|
1583
|
+
"device" -> {
|
|
1584
|
+
// 실제 화면 크기 (세로 기준: 긴 변 / 짧은 변 → h > w)
|
|
1585
|
+
val dm = (activity ?: reactApplicationContext).resources.displayMetrics
|
|
1586
|
+
var sw = dm.widthPixels; var sh = dm.heightPixels
|
|
1587
|
+
if (activity != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
1588
|
+
try {
|
|
1589
|
+
val b = activity.windowManager.currentWindowMetrics.bounds
|
|
1590
|
+
if (b.width() > 0 && b.height() > 0) { sw = b.width(); sh = b.height() }
|
|
1591
|
+
} catch (_: Exception) {}
|
|
1592
|
+
}
|
|
1593
|
+
val lo = minOf(sw, sh); val hi = maxOf(sw, sh)
|
|
1594
|
+
if (lo > 0) hi.toDouble() / lo else 19.5 / 9.0
|
|
1595
|
+
}
|
|
1596
|
+
else -> null
|
|
1597
|
+
}
|
|
1598
|
+
val hDp = if (aspectRatio != null) wDp?.let { it * aspectRatio } else num("height")
|
|
1599
|
+
if (lat == null || lng == null || wDp == null || hDp == null || wDp <= 0 || hDp <= 0 ||
|
|
1600
|
+
lat.isNaN() || lng.isNaN()) {
|
|
1601
|
+
promise.reject("E_INVALID_ARGS", "latitude, longitude, width, height (> 0) are required")
|
|
1602
|
+
return
|
|
1603
|
+
}
|
|
1604
|
+
val myLat = num("myLatitude")
|
|
1605
|
+
val myLng = num("myLongitude")
|
|
1606
|
+
if ((myLat == null) != (myLng == null)) {
|
|
1607
|
+
promise.reject("E_INVALID_ARGS", "myLatitude and myLongitude must be given together")
|
|
1608
|
+
return
|
|
1609
|
+
}
|
|
1610
|
+
if (activity == null) {
|
|
1611
|
+
promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
|
|
1612
|
+
return
|
|
1613
|
+
}
|
|
1614
|
+
val accent = str("themeColor")?.let { try { android.graphics.Color.parseColor(it) } catch (_: Exception) { null } }
|
|
1615
|
+
?: android.graphics.Color.parseColor("#FF6B35")
|
|
1616
|
+
val dark = when (str("appearance")?.lowercase()) {
|
|
1617
|
+
"dark" -> true
|
|
1618
|
+
"light" -> false
|
|
1619
|
+
else -> (activity.resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) ==
|
|
1620
|
+
android.content.res.Configuration.UI_MODE_NIGHT_YES
|
|
1621
|
+
}
|
|
1622
|
+
val wPx = Math.round(com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(wDp.toFloat())).coerceAtLeast(1)
|
|
1623
|
+
val hPx = Math.round(com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(hDp.toFloat())).coerceAtLeast(1)
|
|
1624
|
+
val maxOutW = num("maxWidth")?.takeIf { it > 0 }?.let { Math.round(it).toInt() }
|
|
1625
|
+
val maxOutH = num("maxHeight")?.takeIf { it > 0 }?.let { Math.round(it).toInt() }
|
|
1626
|
+
val req = LocationThumbnailRenderer.Request(
|
|
1627
|
+
lat = lat, lng = lng, widthPx = wPx, heightPx = hPx,
|
|
1628
|
+
density = activity.resources.displayMetrics.density,
|
|
1629
|
+
dark = dark,
|
|
1630
|
+
markerType = str("markerType"),
|
|
1631
|
+
placeCategory = str("placeCategory"),
|
|
1632
|
+
spanMeters = num("spanMeters"),
|
|
1633
|
+
accent = accent,
|
|
1634
|
+
myLat = myLat, myLng = myLng,
|
|
1635
|
+
// 출력 최대 크기 (0 이하·없음 → 기본 512)
|
|
1636
|
+
// 결과 최대 크기 (px). 둘 다 없으면 512 × 512, 하나만 주면 다른 쪽은 제한 없음(0)
|
|
1637
|
+
maxOutWidthPx = maxOutW ?: if (maxOutH != null) 0 else LocationThumbnailRenderer.DEFAULT_MAX_OUT_PX,
|
|
1638
|
+
maxOutHeightPx = maxOutH ?: if (maxOutW != null) 0 else LocationThumbnailRenderer.DEFAULT_MAX_OUT_PX,
|
|
1639
|
+
)
|
|
1640
|
+
com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
|
|
1641
|
+
try {
|
|
1642
|
+
LocationThumbnailRenderer.render(activity, req) { r ->
|
|
1643
|
+
r.fold(
|
|
1644
|
+
onSuccess = { m ->
|
|
1645
|
+
val body = Arguments.createMap()
|
|
1646
|
+
body.putString("uri", m["uri"] as? String)
|
|
1647
|
+
body.putString("path", m["path"] as? String)
|
|
1648
|
+
body.putInt("width", (m["width"] as? Int) ?: wPx)
|
|
1649
|
+
body.putInt("height", (m["height"] as? Int) ?: hPx)
|
|
1650
|
+
body.putBoolean("cached", (m["cached"] as? Boolean) ?: false)
|
|
1651
|
+
promise.resolve(body)
|
|
1652
|
+
},
|
|
1653
|
+
onFailure = { e -> promise.reject("E_THUMBNAIL_FAILED", e.message ?: "Thumbnail render failed", e) }
|
|
1654
|
+
)
|
|
1655
|
+
}
|
|
1656
|
+
} catch (e: Exception) {
|
|
1657
|
+
promise.reject("E_THUMBNAIL_FAILED", e.message ?: "Thumbnail render failed", e)
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/** 지도 썸네일 캐시 비우기 */
|
|
1663
|
+
@ReactMethod
|
|
1664
|
+
fun clearLocationThumbnailCache(promise: Promise) {
|
|
1665
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
1666
|
+
try { LocationThumbnailRenderer.clearCache(reactApplicationContext) } catch (_: Exception) {}
|
|
1667
|
+
promise.resolve(null)
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
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
|
+
|
|
1689
|
+
// ── 역지오코딩 (기기 Geocoder, 디스크 캐시) ──
|
|
1690
|
+
|
|
1691
|
+
@ReactMethod
|
|
1692
|
+
fun reverseGeocode(options: ReadableMap, promise: Promise) {
|
|
1693
|
+
val lat = if (options.hasKey("latitude") && !options.isNull("latitude")) options.getDouble("latitude") else Double.NaN
|
|
1694
|
+
val lng = if (options.hasKey("longitude") && !options.isNull("longitude")) options.getDouble("longitude") else Double.NaN
|
|
1695
|
+
val lang = if (options.hasKey("languageCode") && !options.isNull("languageCode")) options.getString("languageCode") else null
|
|
1696
|
+
ReverseGeocoder.reverseGeocode(reactApplicationContext, lat, lng, lang) { r, e ->
|
|
1697
|
+
if (r != null) {
|
|
1698
|
+
val map = Arguments.createMap()
|
|
1699
|
+
r.address?.let { map.putString("address", it) } ?: map.putNull("address")
|
|
1700
|
+
r.shortAddress?.let { map.putString("shortAddress", it) } ?: map.putNull("shortAddress")
|
|
1701
|
+
map.putBoolean("cached", r.cached)
|
|
1702
|
+
promise.resolve(map)
|
|
1703
|
+
} else {
|
|
1704
|
+
promise.reject(e?.code ?: "E_GEOCODE_FAILED", e?.message ?: "Reverse geocoding failed")
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
@ReactMethod
|
|
1710
|
+
fun clearReverseGeocodeCache(promise: Promise) {
|
|
1711
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
1712
|
+
try { ReverseGeocoder.clearCache(reactApplicationContext) } catch (_: Exception) {}
|
|
1713
|
+
promise.resolve(null)
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1145
1716
|
}
|