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
|
@@ -40,6 +40,33 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
40
40
|
private var maxWidth: Int = 1024
|
|
41
41
|
private var maxHeight: Int = 1024
|
|
42
42
|
private var outputFormat: String = "webp" // Default to webp
|
|
43
|
+
// ─── 동영상 (openGallery mediaType / video 옵션) ───
|
|
44
|
+
private var mediaType: String = "photo"
|
|
45
|
+
private var videoOptions = com.rnattachmentkit.VideoExportOptions()
|
|
46
|
+
/** onSelectionChange 에 실어 보낼 maxSelection (예전엔 10 고정) */
|
|
47
|
+
private var lastMaxSelection: Int = 10
|
|
48
|
+
private var lastThemeColorInt: Int? = null
|
|
49
|
+
private var lastLanguageCode: String = "en"
|
|
50
|
+
@Volatile private var currentVideoExporter: com.rnattachmentkit.VideoExporter? = null
|
|
51
|
+
@Volatile private var videoCancelled = false
|
|
52
|
+
/** 진행 중인 전송 처리 (processingId → 작업). 보내는 중에 갤러리를 다시 열어 또 보낼 수 있어 여러 개일 수 있음 */
|
|
53
|
+
private val activeProcessingJobs = java.util.concurrent.ConcurrentHashMap<String, com.rnattachmentkit.ProcessingJob>()
|
|
54
|
+
/** 가장 최근에 시작한 처리 (cancelVideoProcessing 에 processingId 가 없을 때) */
|
|
55
|
+
@Volatile private var lastProcessingId: String? = null
|
|
56
|
+
|
|
57
|
+
/** 보낸 순간의 설정 — 처리 중에 갤러리를 다시 열면 모듈 필드가 새 호출 값으로 바뀌므로, 처리는 이 사본만 씀 */
|
|
58
|
+
private class SendSnapshot(
|
|
59
|
+
val maxWidth: Int, val maxHeight: Int, val outputFormat: String,
|
|
60
|
+
val videoOptions: com.rnattachmentkit.VideoExportOptions,
|
|
61
|
+
val states: Map<String, com.rnattachmentkit.VideoEditState>,
|
|
62
|
+
val showHUD: Boolean, val themeColor: Int?, val language: String
|
|
63
|
+
)
|
|
64
|
+
private fun sendSnapshot() = SendSnapshot(maxWidth, maxHeight, outputFormat, videoOptions, HashMap(lastVideoEditStates),
|
|
65
|
+
showVideoHUD, lastThemeColorInt, lastLanguageCode)
|
|
66
|
+
/** 동영상 변환 진행 HUD — video.showProgressHUD, 없으면 asyncProcessing 이 아닐 때만 (이벤트로 말풍선에 그리는 앱은 HUD 없이) */
|
|
67
|
+
private var showVideoHUD = true
|
|
68
|
+
/** 시트가 닫힐 때 넘겨받은 동영상 편집 상태 (원본 uri 문자열 → 품질·음소거) */
|
|
69
|
+
private var lastVideoEditStates: Map<String, com.rnattachmentkit.VideoEditState> = emptyMap()
|
|
43
70
|
|
|
44
71
|
// newarch에는 이 맵과 getDefaultAnimationConfig() 구현이 통째로 빠져 있었습니다(oldarch에는 있었음).
|
|
45
72
|
// NativeAttachmentKitSpec(codegen 생성 추상 클래스)이 이 메서드를 abstract로 요구하기 때문에,
|
|
@@ -317,6 +344,22 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
317
344
|
false
|
|
318
345
|
}
|
|
319
346
|
|
|
347
|
+
// 동영상: mediaType "photo"(기본) | "video" | "all", video: VideoOptions
|
|
348
|
+
mediaType = (if (options.hasKey("mediaType") && !options.isNull("mediaType")) options.getString("mediaType") else null)
|
|
349
|
+
?.lowercase()?.takeIf { it == "video" || it == "all" } ?: "photo"
|
|
350
|
+
videoOptions = com.rnattachmentkit.VideoExportOptions.fromMap(
|
|
351
|
+
if (options.hasKey("video") && !options.isNull("video")) options.getMap("video")?.toHashMap() else null
|
|
352
|
+
)
|
|
353
|
+
run {
|
|
354
|
+
val async = options.hasKey("asyncProcessing") && !options.isNull("asyncProcessing") && options.getBoolean("asyncProcessing")
|
|
355
|
+
val v = if (options.hasKey("video") && !options.isNull("video")) options.getMap("video") else null
|
|
356
|
+
showVideoHUD = if (v != null && v.hasKey("showProgressHUD") && !v.isNull("showProgressHUD")) v.getBoolean("showProgressHUD") else !async
|
|
357
|
+
}
|
|
358
|
+
lastMaxSelection = maxSelection
|
|
359
|
+
TurboImagePickerConfig.videoOptions = videoOptions
|
|
360
|
+
lastLanguageCode = languageCode
|
|
361
|
+
lastThemeColorInt = try { themeColor?.let { android.graphics.Color.parseColor(it) } } catch (_: Exception) { null }
|
|
362
|
+
|
|
320
363
|
val fragmentActivity = activity as? FragmentActivity
|
|
321
364
|
if (fragmentActivity == null) {
|
|
322
365
|
promise.reject(E_NO_ACTIVITY, "Activity is not a FragmentActivity")
|
|
@@ -324,31 +367,107 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
324
367
|
}
|
|
325
368
|
|
|
326
369
|
pickImagesPromise = promise
|
|
370
|
+
// 이번 호출의 promise 를 붙잡아 둠 — 리스너가 모듈 필드를 읽으면, 시트가 열린 채 openGallery 를 다시 불렀을 때
|
|
371
|
+
// 이전 시트가 치워지며 오는 onDismiss 가 "새" promise 를 거부해 버림. 시트마다 자기 promise 를 한 번만 끝냄
|
|
372
|
+
val thisPromise = promise
|
|
373
|
+
var settled = false
|
|
374
|
+
val settle: ((Promise) -> Unit) -> Unit = { block ->
|
|
375
|
+
if (!settled) {
|
|
376
|
+
settled = true
|
|
377
|
+
if (pickImagesPromise === thisPromise) pickImagesPromise = null
|
|
378
|
+
block(thisPromise)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// 지난 호출이 남긴 오래된 임시 파일(24시간 지난 것) 정리 — 백그라운드
|
|
382
|
+
moduleScope.launch(Dispatchers.IO) {
|
|
383
|
+
try { TempFileCleaner.cleanStale(reactApplicationContext) } catch (_: Exception) {}
|
|
384
|
+
}
|
|
327
385
|
|
|
328
386
|
val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
329
387
|
Manifest.permission.READ_MEDIA_IMAGES
|
|
330
388
|
} else {
|
|
331
389
|
Manifest.permission.READ_EXTERNAL_STORAGE
|
|
332
390
|
}
|
|
391
|
+
// Android 13+ 에서 동영상도 보이면 READ_MEDIA_VIDEO 를 같이 요청
|
|
392
|
+
val mediaPermissions: Array<String> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && mediaType != "photo") {
|
|
393
|
+
arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO)
|
|
394
|
+
} else {
|
|
395
|
+
arrayOf(permission)
|
|
396
|
+
}
|
|
333
397
|
|
|
334
398
|
val showSheet = {
|
|
335
399
|
fragmentActivity.runOnUiThread {
|
|
336
400
|
try {
|
|
337
401
|
val bottomSheet = ImagePickerBottomSheet.newInstance(
|
|
338
402
|
maxSelection, languageCode, enableEditor, profileMode, maxWidth, maxHeight, themeColor,
|
|
339
|
-
forceMultiMode = forceMultiMode
|
|
403
|
+
forceMultiMode = forceMultiMode,
|
|
404
|
+
// 첨부 메뉴(갤러리 / 위치 / 파일). 기본 false — 켜지 않으면 기존과 같음
|
|
405
|
+
showAttachMenu = options.hasKey("showAttachMenu") && options.getBoolean("showAttachMenu"),
|
|
406
|
+
// (Android) 첨부 메뉴 바 아래 여백 (dp). 없으면 기본 12 / 8
|
|
407
|
+
attachMenuSinkIntoNav = if (options.hasKey("attachMenuSinkIntoNav") && !options.isNull("attachMenuSinkIntoNav"))
|
|
408
|
+
options.getDouble("attachMenuSinkIntoNav").toFloat() else com.rnattachmentkit.AttachMenuBarView.SINK_INTO_NAV_DP.toFloat(),
|
|
409
|
+
attachMenuMinBottomMargin = if (options.hasKey("attachMenuMinBottomMargin") && !options.isNull("attachMenuMinBottomMargin"))
|
|
410
|
+
options.getDouble("attachMenuMinBottomMargin").toFloat() else com.rnattachmentkit.AttachMenuBarView.BOTTOM_MARGIN_DP.toFloat(),
|
|
411
|
+
// 위치 보내기 때 지도 이미지 캡처 여부. 기본 true (기존 그대로). false 면 이미지 없이 위치 정보만
|
|
412
|
+
captureLocationImage = !(options.hasKey("captureLocationImage") && !options.isNull("captureLocationImage")) ||
|
|
413
|
+
options.getBoolean("captureLocationImage"),
|
|
414
|
+
// 동영상 (mediaType "video"/"all" 일 때 그리드에 동영상, 길이 제한)
|
|
415
|
+
mediaType = mediaType,
|
|
416
|
+
maxVideoDurationSec = videoOptions.maxDurationSec
|
|
340
417
|
)
|
|
418
|
+
// 위치 탭에서 보내기 → onLocationSelected 이벤트로 결과를 넘기고, openGallery 는 빈 배열로 끝냄
|
|
419
|
+
bottomSheet.setOnLocationSelectedListener { loc ->
|
|
420
|
+
val body = com.facebook.react.bridge.Arguments.createMap()
|
|
421
|
+
for ((k, v) in loc) {
|
|
422
|
+
when (v) {
|
|
423
|
+
is Int -> body.putInt(k, v)
|
|
424
|
+
is Long -> body.putDouble(k, v.toDouble())
|
|
425
|
+
is Double -> body.putDouble(k, v)
|
|
426
|
+
is Float -> body.putDouble(k, v.toDouble())
|
|
427
|
+
is Boolean -> body.putBoolean(k, v)
|
|
428
|
+
is String -> body.putString(k, v)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
emitGalleryEvent("onLocationSelected", body)
|
|
432
|
+
settle { it.resolve(com.facebook.react.bridge.Arguments.createArray()) }
|
|
433
|
+
}
|
|
434
|
+
// 파일 탭에서 보내기 → onFilesSelected 이벤트 { files: [{uri, path, name, size, mimeType}], failed: [{name, error}] }, openGallery 는 빈 배열
|
|
435
|
+
bottomSheet.setOnFilesResultListener { files, failed ->
|
|
436
|
+
val arr = com.facebook.react.bridge.Arguments.createArray()
|
|
437
|
+
for (file in files) {
|
|
438
|
+
val m = com.facebook.react.bridge.Arguments.createMap()
|
|
439
|
+
for ((k, v) in file) {
|
|
440
|
+
when (v) {
|
|
441
|
+
is Double -> m.putDouble(k, v)
|
|
442
|
+
is String -> m.putString(k, v)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
arr.pushMap(m)
|
|
446
|
+
}
|
|
447
|
+
val body = com.facebook.react.bridge.Arguments.createMap()
|
|
448
|
+
body.putArray("files", arr)
|
|
449
|
+
// 복사에 실패한 파일 (추가 필드 — files 는 예전과 같음)
|
|
450
|
+
val failedArr = com.facebook.react.bridge.Arguments.createArray()
|
|
451
|
+
for (f in failed) {
|
|
452
|
+
val m = com.facebook.react.bridge.Arguments.createMap()
|
|
453
|
+
m.putString("name", f["name"]?.toString() ?: "")
|
|
454
|
+
m.putString("error", f["error"]?.toString() ?: "")
|
|
455
|
+
failedArr.pushMap(m)
|
|
456
|
+
}
|
|
457
|
+
body.putArray("failed", failedArr)
|
|
458
|
+
emitGalleryEvent("onFilesSelected", body)
|
|
459
|
+
settle { it.resolve(com.facebook.react.bridge.Arguments.createArray()) }
|
|
460
|
+
}
|
|
341
461
|
bottomSheet.setOnImagesSelectedListener { uris ->
|
|
462
|
+
lastVideoEditStates = HashMap(bottomSheet.videoEditStates)
|
|
342
463
|
val result = WritableNativeArray()
|
|
343
464
|
for (uri in uris) {
|
|
344
465
|
result.pushString(uri.toString())
|
|
345
466
|
}
|
|
346
|
-
handleSelectedImages(result,
|
|
347
|
-
pickImagesPromise = null
|
|
467
|
+
settle { handleSelectedImages(result, it) }
|
|
348
468
|
}
|
|
349
469
|
bottomSheet.setOnCancelledListener {
|
|
350
|
-
|
|
351
|
-
pickImagesPromise = null
|
|
470
|
+
settle { it.reject(E_PICKER_CANCELLED, "User cancelled image picker") }
|
|
352
471
|
}
|
|
353
472
|
|
|
354
473
|
val existingFragment = fragmentActivity.supportFragmentManager.findFragmentByTag("ImagePickerBottomSheet")
|
|
@@ -361,13 +480,12 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
361
480
|
.commitAllowingStateLoss()
|
|
362
481
|
|
|
363
482
|
} catch (e: Exception) {
|
|
364
|
-
|
|
365
|
-
pickImagesPromise = null
|
|
483
|
+
settle { it.reject(E_FAILED_TO_PICK, "Failed to open image picker: ${e.message}", e) }
|
|
366
484
|
}
|
|
367
485
|
}
|
|
368
486
|
}
|
|
369
487
|
|
|
370
|
-
if (ContextCompat.checkSelfPermission(fragmentActivity,
|
|
488
|
+
if (mediaPermissions.all { ContextCompat.checkSelfPermission(fragmentActivity, it) == PackageManager.PERMISSION_GRANTED }) {
|
|
371
489
|
showSheet()
|
|
372
490
|
} else {
|
|
373
491
|
val permissionAwareActivity = fragmentActivity as? PermissionAwareActivity
|
|
@@ -382,28 +500,25 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
382
500
|
showSheet()
|
|
383
501
|
return
|
|
384
502
|
}
|
|
385
|
-
|
|
503
|
+
mediaPermissions + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED
|
|
386
504
|
} else {
|
|
387
|
-
|
|
505
|
+
mediaPermissions
|
|
388
506
|
}
|
|
389
507
|
|
|
390
508
|
permissionAwareActivity.requestPermissions(permissionsToRequest, 100, object : PermissionListener {
|
|
391
509
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray): Boolean {
|
|
392
510
|
if (requestCode == 100 && grantResults != null) {
|
|
393
511
|
val isGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
(grantResults.size > 1 && grantResults[1] == PackageManager.PERMISSION_GRANTED)
|
|
397
|
-
)
|
|
512
|
+
// 14+: 전체 허용이든 "일부 선택"이든 하나라도 허용이면 진행
|
|
513
|
+
grantResults.any { it == PackageManager.PERMISSION_GRANTED }
|
|
398
514
|
} else {
|
|
399
|
-
grantResults.isNotEmpty() && grantResults
|
|
515
|
+
grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
|
400
516
|
}
|
|
401
517
|
|
|
402
518
|
if (isGranted) {
|
|
403
519
|
showSheet()
|
|
404
520
|
} else {
|
|
405
|
-
|
|
406
|
-
pickImagesPromise = null
|
|
521
|
+
settle { it.reject("E_PERMISSION_DENIED", "Permission denied") }
|
|
407
522
|
}
|
|
408
523
|
}
|
|
409
524
|
return true
|
|
@@ -536,6 +651,22 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
536
651
|
putStringArrayList("placeholderImages", placeholders)
|
|
537
652
|
}
|
|
538
653
|
}
|
|
654
|
+
// 동영상 페이지 (뷰어 동영상 작업 지시서 V1): videos[i] = { uri, cacheFile? } 또는 null(사진).
|
|
655
|
+
// images[i] 는 그 동영상의 포스터. 이 오버레이 뷰어에서만 재생합니다.
|
|
656
|
+
if (options.hasKey("videos")) {
|
|
657
|
+
val videoArray = options.getArray("videos")
|
|
658
|
+
if (videoArray != null) {
|
|
659
|
+
val uris = ArrayList<String>()
|
|
660
|
+
val caches = ArrayList<String>()
|
|
661
|
+
for (i in 0 until videoArray.size()) {
|
|
662
|
+
val m = if (videoArray.getType(i) == com.facebook.react.bridge.ReadableType.Map) videoArray.getMap(i) else null
|
|
663
|
+
uris.add(if (m != null && m.hasKey("uri") && !m.isNull("uri")) m.getString("uri") ?: "" else "")
|
|
664
|
+
caches.add(if (m != null && m.hasKey("cacheFile") && !m.isNull("cacheFile")) m.getString("cacheFile") ?: "" else "")
|
|
665
|
+
}
|
|
666
|
+
putStringArrayList(com.rnattachmentkit.ViewerVideoSource.ARG_URIS, uris)
|
|
667
|
+
putStringArrayList(com.rnattachmentkit.ViewerVideoSource.ARG_CACHE_FILES, caches)
|
|
668
|
+
}
|
|
669
|
+
}
|
|
539
670
|
// 상단바 저장/공유/삭제 버튼과 그 문구들 (iOS와 동일한 옵션 키). 문구를 안 넘기면
|
|
540
671
|
// 네이티브 쪽 로케일 리소스(values-ko/ja/zh, 그 외 en)가 자동 적용됩니다.
|
|
541
672
|
putBoolean("showActionButtons", !options.hasKey("showActionButtons") || options.getBoolean("showActionButtons"))
|
|
@@ -697,6 +828,14 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
697
828
|
}
|
|
698
829
|
}
|
|
699
830
|
|
|
831
|
+
/** 동영상 변환 취소 (HUD 없이 앱이 말풍선에서). index -1 = 남은 동영상 전부. 진행 중인 처리가 없으면 false */
|
|
832
|
+
override fun cancelVideoProcessing(index: Double, processingId: String, promise: Promise) {
|
|
833
|
+
val job = (processingId.ifEmpty { lastProcessingId })?.let { activeProcessingJobs[it] }
|
|
834
|
+
if (job == null) { promise.resolve(false); return }
|
|
835
|
+
job.cancel(if (index >= 0) index.toInt() else null)
|
|
836
|
+
promise.resolve(true)
|
|
837
|
+
}
|
|
838
|
+
|
|
700
839
|
override fun closeGallery(promise: Promise) {
|
|
701
840
|
val activity = getCurrentActivity()
|
|
702
841
|
if (activity == null) {
|
|
@@ -745,9 +884,10 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
745
884
|
return
|
|
746
885
|
}
|
|
747
886
|
|
|
887
|
+
val snap = sendSnapshot()
|
|
748
888
|
moduleScope.launch {
|
|
749
889
|
try {
|
|
750
|
-
val results = processImages(uris)
|
|
890
|
+
val results = processImages(uris, snap)
|
|
751
891
|
if (results.isNotEmpty()) {
|
|
752
892
|
promise.resolve(results[0])
|
|
753
893
|
} else {
|
|
@@ -791,7 +931,7 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
791
931
|
// Process images asynchronously
|
|
792
932
|
val eventMap = com.facebook.react.bridge.WritableNativeMap()
|
|
793
933
|
eventMap.putInt("selectedCount", uris.size)
|
|
794
|
-
eventMap.putInt("maxSelection",
|
|
934
|
+
eventMap.putInt("maxSelection", lastMaxSelection)
|
|
795
935
|
try {
|
|
796
936
|
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
797
937
|
.emit("onSelectionChange", eventMap)
|
|
@@ -799,25 +939,183 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
799
939
|
// Ignore
|
|
800
940
|
}
|
|
801
941
|
|
|
942
|
+
val snap = sendSnapshot()
|
|
802
943
|
moduleScope.launch {
|
|
803
944
|
try {
|
|
804
|
-
val results = processImages(uris)
|
|
945
|
+
val results = processImages(uris, snap)
|
|
805
946
|
val array = com.facebook.react.bridge.Arguments.createArray()
|
|
806
947
|
results.forEach { array.pushMap(it) }
|
|
807
948
|
actualPromise.resolve(array)
|
|
949
|
+
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
950
|
+
actualPromise.reject("CANCELLED", "Video export cancelled")
|
|
808
951
|
} catch (e: Exception) {
|
|
809
952
|
actualPromise.reject(E_FAILED_TO_PICK, "Failed to process images: ${e.message}", e)
|
|
810
953
|
}
|
|
811
954
|
}
|
|
812
955
|
}
|
|
813
956
|
|
|
814
|
-
private suspend fun processImages(uris: List<Uri
|
|
957
|
+
private suspend fun processImages(uris: List<Uri>, snap: SendSnapshot): List<com.facebook.react.bridge.WritableMap> = withContext(Dispatchers.IO) {
|
|
958
|
+
// 보낸 순간의 설정 (모듈 필드와 같은 이름으로 가림 — 아래 코드는 그대로)
|
|
959
|
+
val maxWidth = snap.maxWidth
|
|
960
|
+
val maxHeight = snap.maxHeight
|
|
961
|
+
val outputFormat = snap.outputFormat
|
|
962
|
+
val videoOptions = snap.videoOptions
|
|
963
|
+
val lastVideoEditStates = snap.states
|
|
964
|
+
val showVideoHUD = snap.showHUD
|
|
965
|
+
val lastThemeColorInt = snap.themeColor
|
|
966
|
+
val lastLanguageCode = snap.language
|
|
967
|
+
val processingId = java.util.UUID.randomUUID().toString()
|
|
815
968
|
val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
|
|
816
969
|
val contentResolver = reactApplicationContext.contentResolver
|
|
817
970
|
val totalCount = uris.size
|
|
818
971
|
var currentIndex = 0
|
|
819
972
|
|
|
973
|
+
// 동영상이 섞여 있으면 진행 HUD 를 띄우고, 동영상은 VideoExporter 로 (사진 처리와 같은 루프)
|
|
974
|
+
val isVideoUri: (Uri) -> Boolean = { u ->
|
|
975
|
+
u.toString().contains("/video/") || (try { contentResolver.getType(u)?.startsWith("video/") == true } catch (_: Exception) { false })
|
|
976
|
+
}
|
|
977
|
+
val hasVideo = uris.any(isVideoUri)
|
|
978
|
+
videoCancelled = false
|
|
979
|
+
val job = com.rnattachmentkit.ProcessingJob()
|
|
980
|
+
activeProcessingJobs[processingId] = job
|
|
981
|
+
lastProcessingId = processingId
|
|
982
|
+
// ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
|
|
983
|
+
try {
|
|
984
|
+
val items = Arguments.createArray()
|
|
985
|
+
uris.forEachIndexed { i, u ->
|
|
986
|
+
val it0 = if (isVideoUri(u)) {
|
|
987
|
+
val st = lastVideoEditStates[u.toString()] ?: com.rnattachmentkit.VideoEditState(qualityHeight = videoOptions.defaultQuality)
|
|
988
|
+
com.rnattachmentkit.MediaProcessing.videoItem(reactApplicationContext, i, u, st, videoOptions)
|
|
989
|
+
} else {
|
|
990
|
+
com.rnattachmentkit.MediaProcessing.imageItem(reactApplicationContext, i, u, maxWidth, maxHeight)
|
|
991
|
+
}
|
|
992
|
+
items.pushMap(Arguments.createMap().apply {
|
|
993
|
+
putInt("index", it0.index)
|
|
994
|
+
putString("mediaType", if (it0.isVideo) "video" else "image")
|
|
995
|
+
putString("originalUri", it0.originalUri)
|
|
996
|
+
putInt("width", it0.width)
|
|
997
|
+
putInt("height", it0.height)
|
|
998
|
+
it0.durationSec?.let { d -> putDouble("duration", d) }
|
|
999
|
+
it0.thumbnailUri?.let { t ->
|
|
1000
|
+
putString("thumbnailUri", t)
|
|
1001
|
+
putInt("thumbnailWidth", it0.thumbnailWidth)
|
|
1002
|
+
putInt("thumbnailHeight", it0.thumbnailHeight)
|
|
1003
|
+
}
|
|
1004
|
+
})
|
|
1005
|
+
}
|
|
1006
|
+
emitGalleryEvent("onProcessingStart", Arguments.createMap().apply {
|
|
1007
|
+
putString("processingId", processingId)
|
|
1008
|
+
putInt("total", totalCount)
|
|
1009
|
+
putArray("items", items)
|
|
1010
|
+
})
|
|
1011
|
+
} catch (e: Exception) {
|
|
1012
|
+
android.util.Log.w("RNAttachmentKit", "onProcessingStart failed: ${e.message}")
|
|
1013
|
+
}
|
|
1014
|
+
var hud: com.rnattachmentkit.VideoExportHUD? = null
|
|
1015
|
+
if (hasVideo && showVideoHUD) {
|
|
1016
|
+
withContext(Dispatchers.Main) {
|
|
1017
|
+
val act = getCurrentActivity() ?: return@withContext
|
|
1018
|
+
hud = com.rnattachmentkit.VideoExportHUD(act, lastThemeColorInt, lastLanguageCode).also { h ->
|
|
1019
|
+
h.onCancel = {
|
|
1020
|
+
videoCancelled = true
|
|
1021
|
+
currentVideoExporter?.cancel()
|
|
1022
|
+
}
|
|
1023
|
+
h.show()
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
val videoFiles = mutableListOf<File>()
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
820
1030
|
for (uri in uris) {
|
|
1031
|
+
if (videoCancelled) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1032
|
+
if (isVideoUri(uri)) {
|
|
1033
|
+
val idx = currentIndex
|
|
1034
|
+
val exporter = com.rnattachmentkit.VideoExporter(reactApplicationContext)
|
|
1035
|
+
currentVideoExporter = exporter
|
|
1036
|
+
val cancelledMap = {
|
|
1037
|
+
WritableNativeMap().apply {
|
|
1038
|
+
putString("originalUri", uri.toString())
|
|
1039
|
+
putString("mediaType", "video")
|
|
1040
|
+
putString("error", "CANCELLED")
|
|
1041
|
+
putBoolean("cancelled", true)
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
val resultMap = try {
|
|
1045
|
+
// 앱이 이미 취소한 동영상은 건너뜀
|
|
1046
|
+
if (job.isCancelled(idx)) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1047
|
+
job.begin(idx, exporter)
|
|
1048
|
+
val state = lastVideoEditStates[uri.toString()] ?: com.rnattachmentkit.VideoEditState(qualityHeight = videoOptions.defaultQuality)
|
|
1049
|
+
val throttle = com.rnattachmentkit.ProgressThrottle()
|
|
1050
|
+
val onProgress: (Float) -> Unit = { p ->
|
|
1051
|
+
hud?.let { h -> android.os.Handler(android.os.Looper.getMainLooper()).post { h.update(p, idx, totalCount) } }
|
|
1052
|
+
if (throttle.shouldSend(p)) emitGalleryEvent("onVideoProgress", Arguments.createMap().apply {
|
|
1053
|
+
putInt("index", idx); putInt("total", totalCount); putDouble("progress", p.toDouble())
|
|
1054
|
+
putString("processingId", processingId)
|
|
1055
|
+
})
|
|
1056
|
+
}
|
|
1057
|
+
// 편집 화면에서 미리 만들어 둔 파일이 있으면(또는 만드는 중이면 끝나고) 그대로 씀
|
|
1058
|
+
// 기다리는 동안 앱(X)이나 HUD 가 취소하면 바로 멈춤 (전엔 미리 만들기가 끝날 때까지 못 빠져나왔음)
|
|
1059
|
+
val stop = { videoCancelled || job.isCancelled(idx) }
|
|
1060
|
+
val res = com.rnattachmentkit.VideoPrepareManager.takeReady(uri, state, onProgress, stop)
|
|
1061
|
+
?: run {
|
|
1062
|
+
if (stop()) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1063
|
+
exporter.export(uri, state, videoOptions, reactApplicationContext.cacheDir, "turbo_video_${System.currentTimeMillis()}_$idx", onProgress)
|
|
1064
|
+
}
|
|
1065
|
+
withContext(Dispatchers.Main) { hud?.update(1f, idx, totalCount) }
|
|
1066
|
+
// 미리 만든 파일을 기다리는 동안 취소됐으면 결과를 쓰지 않음
|
|
1067
|
+
if (job.isCancelled(idx)) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1068
|
+
videoFiles += res.file
|
|
1069
|
+
res.thumbnailFile?.let { videoFiles += it }
|
|
1070
|
+
WritableNativeMap().apply {
|
|
1071
|
+
putString("uri", Uri.fromFile(res.file).toString())
|
|
1072
|
+
putString("originalUri", uri.toString())
|
|
1073
|
+
putString("mediaType", "video")
|
|
1074
|
+
putString("type", "video/mp4")
|
|
1075
|
+
putInt("width", res.width)
|
|
1076
|
+
putInt("height", res.height)
|
|
1077
|
+
putDouble("duration", res.durationMs / 1000.0)
|
|
1078
|
+
putDouble("fileSize", res.fileSize.toDouble())
|
|
1079
|
+
putString("fileName", res.file.name)
|
|
1080
|
+
putString("fileExtension", "mp4")
|
|
1081
|
+
putString("videoCodec", res.codec)
|
|
1082
|
+
putInt("videoBitrate", res.bitrate)
|
|
1083
|
+
putBoolean("edited", res.edited)
|
|
1084
|
+
if (res.thumbnailFile != null) {
|
|
1085
|
+
putString("thumbnailUri", Uri.fromFile(res.thumbnailFile).toString())
|
|
1086
|
+
putInt("thumbnailWidth", res.thumbnailWidth)
|
|
1087
|
+
putInt("thumbnailHeight", res.thumbnailHeight)
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
1091
|
+
// 앱이 이 동영상만 취소 → 나머지는 계속. HUD 취소면 전체 취소
|
|
1092
|
+
if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else throw e
|
|
1093
|
+
} catch (e: Exception) {
|
|
1094
|
+
// 취소하면서 변환기가 다른 예외로 끝날 수도 있음 → 취소로 처리
|
|
1095
|
+
if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else {
|
|
1096
|
+
android.util.Log.w("RNAttachmentKit", "video export failed: ${e.message}", e)
|
|
1097
|
+
WritableNativeMap().apply {
|
|
1098
|
+
putString("originalUri", uri.toString())
|
|
1099
|
+
putString("mediaType", "video")
|
|
1100
|
+
putString("error", e.message ?: "export failed")
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
} finally {
|
|
1104
|
+
currentVideoExporter = null
|
|
1105
|
+
job.end()
|
|
1106
|
+
}
|
|
1107
|
+
results.add(resultMap)
|
|
1108
|
+
try {
|
|
1109
|
+
val copy = WritableNativeMap().apply { merge(resultMap) }
|
|
1110
|
+
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1111
|
+
.emit("onImageProcessed", WritableNativeMap().apply {
|
|
1112
|
+
putInt("total", totalCount); putInt("index", idx); putMap("image", copy)
|
|
1113
|
+
putString("processingId", processingId)
|
|
1114
|
+
})
|
|
1115
|
+
} catch (_: Exception) {}
|
|
1116
|
+
currentIndex++
|
|
1117
|
+
continue
|
|
1118
|
+
}
|
|
821
1119
|
try {
|
|
822
1120
|
// 1. Get EXIF Orientation
|
|
823
1121
|
var orientation = android.media.ExifInterface.ORIENTATION_NORMAL
|
|
@@ -948,6 +1246,7 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
948
1246
|
val eventMap = WritableNativeMap().apply {
|
|
949
1247
|
putInt("total", totalCount)
|
|
950
1248
|
putInt("index", currentIndex)
|
|
1249
|
+
putString("processingId", processingId)
|
|
951
1250
|
putMap("image", createImageInfo())
|
|
952
1251
|
}
|
|
953
1252
|
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
@@ -963,6 +1262,14 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
963
1262
|
continue
|
|
964
1263
|
}
|
|
965
1264
|
}
|
|
1265
|
+
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
1266
|
+
// 취소: 지금까지 만든 동영상 파일은 지움
|
|
1267
|
+
videoFiles.forEach { try { it.delete() } catch (_: Exception) {} }
|
|
1268
|
+
throw e
|
|
1269
|
+
} finally {
|
|
1270
|
+
activeProcessingJobs.remove(processingId)
|
|
1271
|
+
if (hud != null) withContext(Dispatchers.Main) { hud?.dismiss() }
|
|
1272
|
+
}
|
|
966
1273
|
|
|
967
1274
|
results
|
|
968
1275
|
}
|
|
@@ -1158,5 +1465,263 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
1158
1465
|
androidx.localbroadcastmanager.content.LocalBroadcastManager.getInstance(reactApplicationContext)
|
|
1159
1466
|
.unregisterReceiver(imageDeletedReceiver)
|
|
1160
1467
|
}
|
|
1161
|
-
}
|
|
1162
1468
|
|
|
1469
|
+
// ── 지도 뷰어 (위치 메시지 → 지도) ──
|
|
1470
|
+
|
|
1471
|
+
private var mapViewer: java.lang.ref.WeakReference<MapViewerDialogFragment>? = null
|
|
1472
|
+
/** updateMapViewerSourceRect 로 갱신되는 닫힘 위치 (화면 px) */
|
|
1473
|
+
@Volatile private var mapViewerSourceRect: android.graphics.Rect? = null
|
|
1474
|
+
|
|
1475
|
+
/** RN measureInWindow 값(dp, 창 기준) → 화면 px Rect */
|
|
1476
|
+
private fun mapRectFromDp(map: ReadableMap?, activity: Activity): android.graphics.Rect? {
|
|
1477
|
+
if (map == null || !map.hasKey("x") || !map.hasKey("y") || !map.hasKey("width") || !map.hasKey("height")) return null
|
|
1478
|
+
val w = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(map.getDouble("width").toFloat())
|
|
1479
|
+
val h = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(map.getDouble("height").toFloat())
|
|
1480
|
+
if (w <= 0f || h <= 0f) return null
|
|
1481
|
+
val loc = IntArray(2)
|
|
1482
|
+
try { activity.window?.decorView?.getLocationOnScreen(loc) } catch (_: Exception) {}
|
|
1483
|
+
val x = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(map.getDouble("x").toFloat()) + loc[0]
|
|
1484
|
+
val y = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(map.getDouble("y").toFloat()) + loc[1]
|
|
1485
|
+
return android.graphics.Rect(Math.round(x), Math.round(y), Math.round(x + w), Math.round(y + h))
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/** 말풍선 이미지 → 로컬 파일 경로. file:// 는 그대로, http(s) 는 Glide 캐시 파일. 실패·지연 시 null (이미지 없이 엶) */
|
|
1489
|
+
private fun mapSourceImagePath(uri: String?): String? {
|
|
1490
|
+
if (uri.isNullOrBlank()) return null
|
|
1491
|
+
if (uri.startsWith("/")) return uri.takeIf { File(it).exists() }
|
|
1492
|
+
if (uri.startsWith("file://")) return Uri.parse(uri).path?.takeIf { File(it).exists() }
|
|
1493
|
+
if (!uri.startsWith("http://") && !uri.startsWith("https://")) return null
|
|
1494
|
+
return try {
|
|
1495
|
+
com.bumptech.glide.Glide.with(reactApplicationContext.applicationContext)
|
|
1496
|
+
.downloadOnly().load(uri).submit()
|
|
1497
|
+
.get(1500, java.util.concurrent.TimeUnit.MILLISECONDS)
|
|
1498
|
+
?.absolutePath
|
|
1499
|
+
} catch (e: Exception) {
|
|
1500
|
+
android.util.Log.w("RNAttachmentKit", "openMapViewer: source image not loaded (${e.javaClass.simpleName})")
|
|
1501
|
+
null
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
override fun openMapViewer(options: ReadableMap, promise: Promise) {
|
|
1506
|
+
val activity = getCurrentActivity() as? FragmentActivity
|
|
1507
|
+
if (activity == null) {
|
|
1508
|
+
promise.reject("NO_ACTIVITY", "Activity is not a FragmentActivity")
|
|
1509
|
+
return
|
|
1510
|
+
}
|
|
1511
|
+
if (!options.hasKey("latitude") || !options.hasKey("longitude")) {
|
|
1512
|
+
promise.reject("ERROR", "latitude and longitude are required")
|
|
1513
|
+
return
|
|
1514
|
+
}
|
|
1515
|
+
fun str(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getString(key) else null
|
|
1516
|
+
fun num(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getDouble(key) else null
|
|
1517
|
+
val lat = options.getDouble("latitude")
|
|
1518
|
+
val lng = options.getDouble("longitude")
|
|
1519
|
+
val rect = mapRectFromDp(if (options.hasKey("sourceRect") && !options.isNull("sourceRect")) options.getMap("sourceRect") else null, activity)
|
|
1520
|
+
val imageUri = str("sourceImageUri")
|
|
1521
|
+
val themeColor = str("themeColor")?.let { try { android.graphics.Color.parseColor(it) } catch (_: Exception) { null } }
|
|
1522
|
+
val radiusPx = com.facebook.react.uimanager.PixelUtil.toPixelFromDIP((num("sourceCornerRadius") ?: 12.0).toFloat())
|
|
1523
|
+
val showDirections = if (options.hasKey("showDirections") && !options.isNull("showDirections")) options.getBoolean("showDirections") else true
|
|
1524
|
+
|
|
1525
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
1526
|
+
val imagePath = if (rect != null) mapSourceImagePath(imageUri) else null
|
|
1527
|
+
withContext(Dispatchers.Main) {
|
|
1528
|
+
if (activity.isFinishing || activity.isDestroyed || activity.supportFragmentManager.isStateSaved) {
|
|
1529
|
+
promise.reject("ERROR", "Activity is not available")
|
|
1530
|
+
return@withContext
|
|
1531
|
+
}
|
|
1532
|
+
try {
|
|
1533
|
+
mapViewerSourceRect = rect
|
|
1534
|
+
val f = MapViewerDialogFragment.show(
|
|
1535
|
+
activity, lat, lng,
|
|
1536
|
+
title = str("title"),
|
|
1537
|
+
address = str("address"),
|
|
1538
|
+
markerType = str("markerType"),
|
|
1539
|
+
themeColor = themeColor,
|
|
1540
|
+
languageCode = str("languageCode") ?: "en",
|
|
1541
|
+
showDirections = showDirections,
|
|
1542
|
+
sourceRect = rect,
|
|
1543
|
+
sourceImagePath = imagePath,
|
|
1544
|
+
sourceCornerRadius = radiusPx,
|
|
1545
|
+
spanMeters = num("spanMeters") ?: 0.0,
|
|
1546
|
+
appearance = str("appearance"),
|
|
1547
|
+
addressLanguageCode = str("addressLanguageCode"),
|
|
1548
|
+
)
|
|
1549
|
+
if (rect != null) f.sourceRectProvider = { mapViewerSourceRect }
|
|
1550
|
+
var settled = false
|
|
1551
|
+
f.onWillClose = { ms ->
|
|
1552
|
+
try {
|
|
1553
|
+
reactApplicationContext
|
|
1554
|
+
.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1555
|
+
.emit("onMapViewerWillClose", Arguments.createMap().apply { putInt("durationMs", ms) })
|
|
1556
|
+
} catch (_: Exception) {}
|
|
1557
|
+
}
|
|
1558
|
+
f.onClosed = {
|
|
1559
|
+
if (!settled) { settled = true; promise.resolve(null) }
|
|
1560
|
+
mapViewerSourceRect = null
|
|
1561
|
+
mapViewer = null
|
|
1562
|
+
}
|
|
1563
|
+
mapViewer = java.lang.ref.WeakReference(f)
|
|
1564
|
+
} catch (e: Exception) {
|
|
1565
|
+
promise.reject("ERROR", e.message, e)
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
override fun updateMapViewerSourceRect(rect: ReadableMap, promise: Promise) {
|
|
1572
|
+
val activity = getCurrentActivity()
|
|
1573
|
+
if (mapViewer?.get() != null && activity != null) mapViewerSourceRect = mapRectFromDp(rect, activity)
|
|
1574
|
+
promise.resolve(null)
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* 위치 메시지 지도 썸네일을 이 기기에서 그림 (테마별 캐시). 결과 { uri, path, width, height(px), cached }
|
|
1579
|
+
* options: latitude, longitude, width, height (dp, 필수) / markerType, placeCategory, spanMeters, themeColor,
|
|
1580
|
+
* appearance ("light"/"dark"/"system"), myLatitude + myLongitude (둘 다 있을 때만)
|
|
1581
|
+
* aspect ("square" | "portrait" | "device"): 주면 width 만 필수, height 는 비율로 (square = 1:1,
|
|
1582
|
+
* portrait = 480:1040 보내던 이미지와 같은 9:19.5, device = 실제 화면 긴 변/짧은 변). 모르는 값이면 없는 것과 같음
|
|
1583
|
+
* maxWidth / maxHeight (px, 기본 512, 0 이하면 무시): 출력 WebP 가 이 안에 들어가게 비율대로 줄임
|
|
1584
|
+
* (k = min(density, maxWidth / widthDp, maxHeight / heightDp)) → 결과 width/height 는 출력 px
|
|
1585
|
+
*/
|
|
1586
|
+
@ReactMethod
|
|
1587
|
+
override fun renderLocationThumbnail(options: ReadableMap, promise: Promise) {
|
|
1588
|
+
fun str(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getString(key) else null
|
|
1589
|
+
fun num(key: String) = if (options.hasKey(key) && !options.isNull(key)) options.getDouble(key) else null
|
|
1590
|
+
val lat = num("latitude")
|
|
1591
|
+
val lng = num("longitude")
|
|
1592
|
+
val wDp = num("width")
|
|
1593
|
+
val activity = getCurrentActivity()
|
|
1594
|
+
val aspectRatio: Double? = when (str("aspect")?.lowercase()) {
|
|
1595
|
+
"square" -> 1.0
|
|
1596
|
+
"portrait" -> 1040.0 / 480.0
|
|
1597
|
+
"device" -> {
|
|
1598
|
+
// 실제 화면 크기 (세로 기준: 긴 변 / 짧은 변 → h > w)
|
|
1599
|
+
val dm = (activity ?: reactApplicationContext).resources.displayMetrics
|
|
1600
|
+
var sw = dm.widthPixels; var sh = dm.heightPixels
|
|
1601
|
+
if (activity != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
1602
|
+
try {
|
|
1603
|
+
val b = activity.windowManager.currentWindowMetrics.bounds
|
|
1604
|
+
if (b.width() > 0 && b.height() > 0) { sw = b.width(); sh = b.height() }
|
|
1605
|
+
} catch (_: Exception) {}
|
|
1606
|
+
}
|
|
1607
|
+
val lo = minOf(sw, sh); val hi = maxOf(sw, sh)
|
|
1608
|
+
if (lo > 0) hi.toDouble() / lo else 19.5 / 9.0
|
|
1609
|
+
}
|
|
1610
|
+
else -> null
|
|
1611
|
+
}
|
|
1612
|
+
val hDp = if (aspectRatio != null) wDp?.let { it * aspectRatio } else num("height")
|
|
1613
|
+
if (lat == null || lng == null || wDp == null || hDp == null || wDp <= 0 || hDp <= 0 ||
|
|
1614
|
+
lat.isNaN() || lng.isNaN()) {
|
|
1615
|
+
promise.reject("E_INVALID_ARGS", "latitude, longitude, width, height (> 0) are required")
|
|
1616
|
+
return
|
|
1617
|
+
}
|
|
1618
|
+
val myLat = num("myLatitude")
|
|
1619
|
+
val myLng = num("myLongitude")
|
|
1620
|
+
if ((myLat == null) != (myLng == null)) {
|
|
1621
|
+
promise.reject("E_INVALID_ARGS", "myLatitude and myLongitude must be given together")
|
|
1622
|
+
return
|
|
1623
|
+
}
|
|
1624
|
+
if (activity == null) {
|
|
1625
|
+
promise.reject(E_NO_ACTIVITY, "Activity doesn't exist")
|
|
1626
|
+
return
|
|
1627
|
+
}
|
|
1628
|
+
val accent = str("themeColor")?.let { try { android.graphics.Color.parseColor(it) } catch (_: Exception) { null } }
|
|
1629
|
+
?: android.graphics.Color.parseColor("#FF6B35")
|
|
1630
|
+
val dark = when (str("appearance")?.lowercase()) {
|
|
1631
|
+
"dark" -> true
|
|
1632
|
+
"light" -> false
|
|
1633
|
+
else -> (activity.resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK) ==
|
|
1634
|
+
android.content.res.Configuration.UI_MODE_NIGHT_YES
|
|
1635
|
+
}
|
|
1636
|
+
val wPx = Math.round(com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(wDp.toFloat())).coerceAtLeast(1)
|
|
1637
|
+
val hPx = Math.round(com.facebook.react.uimanager.PixelUtil.toPixelFromDIP(hDp.toFloat())).coerceAtLeast(1)
|
|
1638
|
+
val maxOutW = num("maxWidth")?.takeIf { it > 0 }?.let { Math.round(it).toInt() }
|
|
1639
|
+
val maxOutH = num("maxHeight")?.takeIf { it > 0 }?.let { Math.round(it).toInt() }
|
|
1640
|
+
val req = LocationThumbnailRenderer.Request(
|
|
1641
|
+
lat = lat, lng = lng, widthPx = wPx, heightPx = hPx,
|
|
1642
|
+
density = activity.resources.displayMetrics.density,
|
|
1643
|
+
dark = dark,
|
|
1644
|
+
markerType = str("markerType"),
|
|
1645
|
+
placeCategory = str("placeCategory"),
|
|
1646
|
+
spanMeters = num("spanMeters"),
|
|
1647
|
+
accent = accent,
|
|
1648
|
+
myLat = myLat, myLng = myLng,
|
|
1649
|
+
// 출력 최대 크기 (0 이하·없음 → 기본 512)
|
|
1650
|
+
// 결과 최대 크기 (px). 둘 다 없으면 512 × 512, 하나만 주면 다른 쪽은 제한 없음(0)
|
|
1651
|
+
maxOutWidthPx = maxOutW ?: if (maxOutH != null) 0 else LocationThumbnailRenderer.DEFAULT_MAX_OUT_PX,
|
|
1652
|
+
maxOutHeightPx = maxOutH ?: if (maxOutW != null) 0 else LocationThumbnailRenderer.DEFAULT_MAX_OUT_PX,
|
|
1653
|
+
)
|
|
1654
|
+
com.facebook.react.bridge.UiThreadUtil.runOnUiThread {
|
|
1655
|
+
try {
|
|
1656
|
+
LocationThumbnailRenderer.render(activity, req) { r ->
|
|
1657
|
+
r.fold(
|
|
1658
|
+
onSuccess = { m ->
|
|
1659
|
+
val body = Arguments.createMap()
|
|
1660
|
+
body.putString("uri", m["uri"] as? String)
|
|
1661
|
+
body.putString("path", m["path"] as? String)
|
|
1662
|
+
body.putInt("width", (m["width"] as? Int) ?: wPx)
|
|
1663
|
+
body.putInt("height", (m["height"] as? Int) ?: hPx)
|
|
1664
|
+
body.putBoolean("cached", (m["cached"] as? Boolean) ?: false)
|
|
1665
|
+
promise.resolve(body)
|
|
1666
|
+
},
|
|
1667
|
+
onFailure = { e -> promise.reject("E_THUMBNAIL_FAILED", e.message ?: "Thumbnail render failed", e) }
|
|
1668
|
+
)
|
|
1669
|
+
}
|
|
1670
|
+
} catch (e: Exception) {
|
|
1671
|
+
promise.reject("E_THUMBNAIL_FAILED", e.message ?: "Thumbnail render failed", e)
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/** 지도 썸네일 캐시 비우기 */
|
|
1677
|
+
@ReactMethod
|
|
1678
|
+
override fun clearLocationThumbnailCache(promise: Promise) {
|
|
1679
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
1680
|
+
try { LocationThumbnailRenderer.clearCache(reactApplicationContext) } catch (_: Exception) {}
|
|
1681
|
+
promise.resolve(null)
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// ── AI 기능 설정·잔액 (넘긴 값만 바뀜). 사용·충전 요청은 onAIUsage / onAIChargeRequired 이벤트로 알림 ──
|
|
1686
|
+
|
|
1687
|
+
override fun setAIConfig(config: ReadableMap, promise: Promise) {
|
|
1688
|
+
AIBilling.apply(config.toHashMap())
|
|
1689
|
+
val ctx = reactApplicationContext
|
|
1690
|
+
fun emit(name: String, body: Map<String, Any>) {
|
|
1691
|
+
// 앱이 내려가는 중이면 조용히 무시
|
|
1692
|
+
runCatching {
|
|
1693
|
+
ctx.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1694
|
+
.emit(name, com.facebook.react.bridge.Arguments.makeNativeMap(body))
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
AIBilling.onUsage = { emit("onAIUsage", it) }
|
|
1698
|
+
AIBilling.onChargeRequired = { emit("onAIChargeRequired", it) }
|
|
1699
|
+
promise.resolve(null)
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// ── 역지오코딩 (기기 Geocoder, 디스크 캐시) ──
|
|
1703
|
+
|
|
1704
|
+
override fun reverseGeocode(options: ReadableMap, promise: Promise) {
|
|
1705
|
+
val lat = if (options.hasKey("latitude") && !options.isNull("latitude")) options.getDouble("latitude") else Double.NaN
|
|
1706
|
+
val lng = if (options.hasKey("longitude") && !options.isNull("longitude")) options.getDouble("longitude") else Double.NaN
|
|
1707
|
+
val lang = if (options.hasKey("languageCode") && !options.isNull("languageCode")) options.getString("languageCode") else null
|
|
1708
|
+
ReverseGeocoder.reverseGeocode(reactApplicationContext, lat, lng, lang) { r, e ->
|
|
1709
|
+
if (r != null) {
|
|
1710
|
+
val map = Arguments.createMap()
|
|
1711
|
+
r.address?.let { map.putString("address", it) } ?: map.putNull("address")
|
|
1712
|
+
r.shortAddress?.let { map.putString("shortAddress", it) } ?: map.putNull("shortAddress")
|
|
1713
|
+
map.putBoolean("cached", r.cached)
|
|
1714
|
+
promise.resolve(map)
|
|
1715
|
+
} else {
|
|
1716
|
+
promise.reject(e?.code ?: "E_GEOCODE_FAILED", e?.message ?: "Reverse geocoding failed")
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
override fun clearReverseGeocodeCache(promise: Promise) {
|
|
1722
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
1723
|
+
try { ReverseGeocoder.clearCache(reactApplicationContext) } catch (_: Exception) {}
|
|
1724
|
+
promise.resolve(null)
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|