react-native-attachment-kit 1.0.17 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +472 -22
- package/android/build.gradle +8 -3
- package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
- package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +60 -341
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +60 -341
- package/ios/RNAttachmentKit.swift +66 -771
- package/ios/RNAttachmentKit.xcframework/Info.plist +5 -5
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +6853 -669
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +108 -0
- 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 +108 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +12 -12
- 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/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +6853 -669
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +108 -0
- 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 +108 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +6853 -669
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +108 -0
- 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 +108 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +22 -22
- package/package.json +1 -1
- package/src/types.ts +5 -0
package/android/build.gradle
CHANGED
|
@@ -97,7 +97,10 @@ if (project != rootProject) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
dependencies {
|
|
100
|
-
|
|
100
|
+
// 1.1.0: aar 를 `api` 로 — 앱의 네이티브 코드도 키트 공개 클래스(AttachmentProcessor·ImagePickerBottomSheet·
|
|
101
|
+
// ImageViewerOverlayView·VideoExporter …)를 바로 쓸 수 있게 (전엔 앱이 같은 aar 를 compileOnly 로 따로 잡아야 했음).
|
|
102
|
+
// 클래스 이름은 aar 의 proguard-rules.pro(-keep class com.rnattachmentkit.**)가 그대로 둠
|
|
103
|
+
api files('src/main/libs/rnattachmentkit-release.aar')
|
|
101
104
|
compileOnly "com.facebook.react:react-android:+"
|
|
102
105
|
|
|
103
106
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.0"
|
|
@@ -107,7 +110,8 @@ if (project != rootProject) {
|
|
|
107
110
|
implementation "androidx.fragment:fragment-ktx:1.6.2"
|
|
108
111
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
|
|
109
112
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
|
|
110
|
-
|
|
113
|
+
// 공개 클래스의 부모 타입(BottomSheetDialogFragment 등)이라 앱 컴파일 경로에도 보여야 함
|
|
114
|
+
api "com.google.android.material:material:1.11.0"
|
|
111
115
|
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
|
112
116
|
implementation "androidx.cardview:cardview:1.0.0"
|
|
113
117
|
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
|
|
@@ -132,7 +136,8 @@ if (project != rootProject) {
|
|
|
132
136
|
// 이미지 뷰어 동영상: HLS(.m3u8) 스트리밍 재생 (iOS AVPlayer 와 맞춤)
|
|
133
137
|
implementation "androidx.media3:media3-exoplayer-hls:${media3_version}"
|
|
134
138
|
implementation "androidx.media3:media3-ui:${media3_version}"
|
|
135
|
-
|
|
139
|
+
// VideoExporter 공개 API 가 쓰는 타입 — 앱 컴파일 경로에도 보여야 함
|
|
140
|
+
api "androidx.media3:media3-transformer:${media3_version}"
|
|
136
141
|
implementation "androidx.media3:media3-effect:${media3_version}"
|
|
137
142
|
|
|
138
143
|
// CameraX dependencies — 1.4.0+ 부터 libimage_processing_util_jni.so 가 16KB 정렬 (AAR 쪽과 같은 버전)
|
|
Binary file
|
|
@@ -47,8 +47,6 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
47
47
|
private var lastMaxSelection: Int = 10
|
|
48
48
|
private var lastThemeColorInt: Int? = null
|
|
49
49
|
private var lastLanguageCode: String = "en"
|
|
50
|
-
@Volatile private var currentVideoExporter: com.rnattachmentkit.VideoExporter? = null
|
|
51
|
-
@Volatile private var videoCancelled = false
|
|
52
50
|
/** 진행 중인 전송 처리 (processingId → 작업). 보내는 중에 갤러리를 다시 열어 또 보낼 수 있어 여러 개일 수 있음 */
|
|
53
51
|
private val activeProcessingJobs = java.util.concurrent.ConcurrentHashMap<String, com.rnattachmentkit.ProcessingJob>()
|
|
54
52
|
/** 가장 최근에 시작한 처리 (cancelVideoProcessing 에 processingId 가 없을 때) */
|
|
@@ -699,6 +697,8 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
699
697
|
// 네이티브 쪽 로케일 리소스(values-ko/ja/zh, 그 외 en)가 자동 적용됩니다.
|
|
700
698
|
putBoolean("showActionButtons", !options.hasKey("showActionButtons") || options.getBoolean("showActionButtons"))
|
|
701
699
|
putBoolean("showDeleteButton", !options.hasKey("showDeleteButton") || options.getBoolean("showDeleteButton"))
|
|
700
|
+
// 처음에 상단바·하단바·동영상 조작 UI 를 숨긴 채로 엶 (탭하면 보임)
|
|
701
|
+
putBoolean("startWithUiHidden", options.hasKey("startWithUiHidden") && options.getBoolean("startWithUiHidden"))
|
|
702
702
|
for (key in listOf(
|
|
703
703
|
"saveAllText", "saveOneText", "shareAllText", "shareOneText", "actionCancelText",
|
|
704
704
|
"deleteConfirmTitle", "deleteConfirmMessage", "deleteConfirmGroupMessage", "deleteButtonText"
|
|
@@ -983,374 +983,93 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
983
983
|
}
|
|
984
984
|
}
|
|
985
985
|
|
|
986
|
+
/** 고른 뒤 처리 — com.rnattachmentkit.AttachmentProcessor (앱 네이티브 화면과 같은 코드). 여기선 RN 이벤트·HUD 만 */
|
|
986
987
|
private suspend fun processImages(uris: List<Uri>, snap: SendSnapshot): List<com.facebook.react.bridge.WritableMap> = withContext(Dispatchers.IO) {
|
|
987
|
-
// 보낸 순간의 설정 (모듈 필드와 같은 이름으로 가림 — 아래 코드는 그대로)
|
|
988
|
-
val maxWidth = snap.maxWidth
|
|
989
|
-
val maxHeight = snap.maxHeight
|
|
990
|
-
val outputFormat = snap.outputFormat
|
|
991
|
-
val videoOptions = snap.videoOptions
|
|
992
|
-
val lastVideoEditStates = snap.states
|
|
993
|
-
val showVideoHUD = snap.showHUD
|
|
994
|
-
val lastThemeColorInt = snap.themeColor
|
|
995
|
-
val lastLanguageCode = snap.language
|
|
996
|
-
val thumbnailOptions = snap.thumbnail
|
|
997
988
|
val processingId = java.util.UUID.randomUUID().toString()
|
|
998
|
-
val results = mutableListOf<com.facebook.react.bridge.WritableMap>()
|
|
999
|
-
val contentResolver = reactApplicationContext.contentResolver
|
|
1000
989
|
val totalCount = uris.size
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
val hasVideo = uris.any(isVideoUri)
|
|
1008
|
-
videoCancelled = false
|
|
990
|
+
val options = com.rnattachmentkit.AttachmentProcessor.Options(
|
|
991
|
+
maxWidth = snap.maxWidth, maxHeight = snap.maxHeight, outputFormat = snap.outputFormat,
|
|
992
|
+
thumbnail = snap.thumbnail, videoOptions = snap.videoOptions,
|
|
993
|
+
)
|
|
994
|
+
val hasVideo = uris.any { com.rnattachmentkit.AttachmentProcessor.isVideoUri(reactApplicationContext, it) }
|
|
995
|
+
// 앱의 cancelVideoProcessing(index) → job.cancel, HUD 의 취소 → job.abort (전체 취소)
|
|
1009
996
|
val job = com.rnattachmentkit.ProcessingJob()
|
|
1010
997
|
activeProcessingJobs[processingId] = job
|
|
1011
998
|
lastProcessingId = processingId
|
|
1012
|
-
// ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
|
|
1013
|
-
try {
|
|
1014
|
-
val items = Arguments.createArray()
|
|
1015
|
-
uris.forEachIndexed { i, u ->
|
|
1016
|
-
val it0 = if (isVideoUri(u)) {
|
|
1017
|
-
val st = lastVideoEditStates[u.toString()] ?: com.rnattachmentkit.VideoEditState(qualityHeight = videoOptions.defaultQuality)
|
|
1018
|
-
com.rnattachmentkit.MediaProcessing.videoItem(reactApplicationContext, i, u, st, videoOptions)
|
|
1019
|
-
} else {
|
|
1020
|
-
com.rnattachmentkit.MediaProcessing.imageItem(reactApplicationContext, i, u, maxWidth, maxHeight)
|
|
1021
|
-
}
|
|
1022
|
-
items.pushMap(Arguments.createMap().apply {
|
|
1023
|
-
putInt("index", it0.index)
|
|
1024
|
-
putString("mediaType", if (it0.isVideo) "video" else "image")
|
|
1025
|
-
putString("originalUri", it0.originalUri)
|
|
1026
|
-
putInt("width", it0.width)
|
|
1027
|
-
putInt("height", it0.height)
|
|
1028
|
-
it0.durationSec?.let { d -> putDouble("duration", d) }
|
|
1029
|
-
it0.thumbnailUri?.let { t ->
|
|
1030
|
-
putString("thumbnailUri", t)
|
|
1031
|
-
putInt("thumbnailWidth", it0.thumbnailWidth)
|
|
1032
|
-
putInt("thumbnailHeight", it0.thumbnailHeight)
|
|
1033
|
-
}
|
|
1034
|
-
})
|
|
1035
|
-
}
|
|
1036
|
-
emitGalleryEvent("onProcessingStart", Arguments.createMap().apply {
|
|
1037
|
-
putString("processingId", processingId)
|
|
1038
|
-
putInt("total", totalCount)
|
|
1039
|
-
putArray("items", items)
|
|
1040
|
-
})
|
|
1041
|
-
} catch (e: Exception) {
|
|
1042
|
-
android.util.Log.w("RNAttachmentKit", "onProcessingStart failed: ${e.message}")
|
|
1043
|
-
}
|
|
1044
999
|
var hud: com.rnattachmentkit.VideoExportHUD? = null
|
|
1045
|
-
if (hasVideo &&
|
|
1000
|
+
if (hasVideo && snap.showHUD) {
|
|
1046
1001
|
withContext(Dispatchers.Main) {
|
|
1047
1002
|
val act = getCurrentActivity() ?: return@withContext
|
|
1048
|
-
hud = com.rnattachmentkit.VideoExportHUD(act,
|
|
1049
|
-
h.onCancel = {
|
|
1050
|
-
videoCancelled = true
|
|
1051
|
-
currentVideoExporter?.cancel()
|
|
1052
|
-
}
|
|
1003
|
+
hud = com.rnattachmentkit.VideoExportHUD(act, snap.themeColor, snap.language).also { h ->
|
|
1004
|
+
h.onCancel = { job.abort() }
|
|
1053
1005
|
h.show()
|
|
1054
1006
|
}
|
|
1055
1007
|
}
|
|
1056
1008
|
}
|
|
1057
|
-
val
|
|
1058
|
-
|
|
1009
|
+
val main = android.os.Handler(android.os.Looper.getMainLooper())
|
|
1010
|
+
val emitter = { reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) }
|
|
1059
1011
|
try {
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
val throttle = com.rnattachmentkit.ProgressThrottle()
|
|
1080
|
-
val onProgress: (Float) -> Unit = { p ->
|
|
1081
|
-
hud?.let { h -> android.os.Handler(android.os.Looper.getMainLooper()).post { h.update(p, idx, totalCount) } }
|
|
1082
|
-
if (throttle.shouldSend(p)) emitGalleryEvent("onVideoProgress", Arguments.createMap().apply {
|
|
1083
|
-
putInt("index", idx); putInt("total", totalCount); putDouble("progress", p.toDouble())
|
|
1084
|
-
putString("processingId", processingId)
|
|
1085
|
-
})
|
|
1086
|
-
}
|
|
1087
|
-
// 편집 화면에서 미리 만들어 둔 파일이 있으면(또는 만드는 중이면 끝나고) 그대로 씀
|
|
1088
|
-
// 기다리는 동안 앱(X)이나 HUD 가 취소하면 바로 멈춤 (전엔 미리 만들기가 끝날 때까지 못 빠져나왔음)
|
|
1089
|
-
val stop = { videoCancelled || job.isCancelled(idx) }
|
|
1090
|
-
val res = com.rnattachmentkit.VideoPrepareManager.takeReady(uri, state, onProgress, stop)
|
|
1091
|
-
?: run {
|
|
1092
|
-
if (stop()) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1093
|
-
exporter.export(uri, state, videoOptions, reactApplicationContext.cacheDir, "turbo_video_${System.currentTimeMillis()}_$idx", onProgress)
|
|
1094
|
-
}
|
|
1095
|
-
withContext(Dispatchers.Main) { hud?.update(1f, idx, totalCount) }
|
|
1096
|
-
// 미리 만든 파일을 기다리는 동안 취소됐으면 결과를 쓰지 않음
|
|
1097
|
-
if (job.isCancelled(idx)) throw com.rnattachmentkit.VideoExporter.CancelledException()
|
|
1098
|
-
videoFiles += res.file
|
|
1099
|
-
res.thumbnailFile?.let { videoFiles += it }
|
|
1100
|
-
WritableNativeMap().apply {
|
|
1101
|
-
putString("uri", Uri.fromFile(res.file).toString())
|
|
1102
|
-
putString("originalUri", uri.toString())
|
|
1103
|
-
putString("mediaType", "video")
|
|
1104
|
-
putString("type", "video/mp4")
|
|
1105
|
-
putInt("width", res.width)
|
|
1106
|
-
putInt("height", res.height)
|
|
1107
|
-
putDouble("duration", res.durationMs / 1000.0)
|
|
1108
|
-
putDouble("fileSize", res.fileSize.toDouble())
|
|
1109
|
-
putString("fileName", res.file.name)
|
|
1110
|
-
putString("fileExtension", "mp4")
|
|
1111
|
-
putString("videoCodec", res.codec)
|
|
1112
|
-
putInt("videoBitrate", res.bitrate)
|
|
1113
|
-
putBoolean("edited", res.edited)
|
|
1114
|
-
if (res.thumbnailFile != null) {
|
|
1115
|
-
putString("thumbnailUri", Uri.fromFile(res.thumbnailFile).toString())
|
|
1116
|
-
putInt("thumbnailWidth", res.thumbnailWidth)
|
|
1117
|
-
putInt("thumbnailHeight", res.thumbnailHeight)
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
1121
|
-
// 앱이 이 동영상만 취소 → 나머지는 계속. HUD 취소면 전체 취소
|
|
1122
|
-
if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else throw e
|
|
1123
|
-
} catch (e: Exception) {
|
|
1124
|
-
// 취소하면서 변환기가 다른 예외로 끝날 수도 있음 → 취소로 처리
|
|
1125
|
-
if (job.isCancelled(idx) && !videoCancelled) cancelledMap() else {
|
|
1126
|
-
android.util.Log.w("RNAttachmentKit", "video export failed: ${e.message}", e)
|
|
1127
|
-
WritableNativeMap().apply {
|
|
1128
|
-
putString("originalUri", uri.toString())
|
|
1129
|
-
putString("mediaType", "video")
|
|
1130
|
-
putString("error", e.message ?: "export failed")
|
|
1012
|
+
com.rnattachmentkit.AttachmentProcessor.process(reactApplicationContext, uris, snap.states, options, job,
|
|
1013
|
+
object : com.rnattachmentkit.AttachmentProcessor.Listener {
|
|
1014
|
+
// ✅ 처리 시작: 모든 자리(사진·동영상)를 먼저 알림 → 앱이 말풍선을 바로 만들고 진행률을 그림
|
|
1015
|
+
override fun onStart(items: List<com.rnattachmentkit.MediaProcessing.StartItem>) {
|
|
1016
|
+
val arr = Arguments.createArray()
|
|
1017
|
+
items.forEach { it0 ->
|
|
1018
|
+
arr.pushMap(Arguments.createMap().apply {
|
|
1019
|
+
putInt("index", it0.index)
|
|
1020
|
+
putString("mediaType", if (it0.isVideo) "video" else "image")
|
|
1021
|
+
putString("originalUri", it0.originalUri)
|
|
1022
|
+
putInt("width", it0.width)
|
|
1023
|
+
putInt("height", it0.height)
|
|
1024
|
+
it0.durationSec?.let { d -> putDouble("duration", d) }
|
|
1025
|
+
it0.thumbnailUri?.let { t ->
|
|
1026
|
+
putString("thumbnailUri", t)
|
|
1027
|
+
putInt("thumbnailWidth", it0.thumbnailWidth)
|
|
1028
|
+
putInt("thumbnailHeight", it0.thumbnailHeight)
|
|
1029
|
+
}
|
|
1030
|
+
})
|
|
1131
1031
|
}
|
|
1132
|
-
|
|
1133
|
-
} finally {
|
|
1134
|
-
currentVideoExporter = null
|
|
1135
|
-
job.end()
|
|
1136
|
-
}
|
|
1137
|
-
results.add(resultMap)
|
|
1138
|
-
try {
|
|
1139
|
-
val copy = WritableNativeMap().apply { merge(resultMap) }
|
|
1140
|
-
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1141
|
-
.emit("onImageProcessed", WritableNativeMap().apply {
|
|
1142
|
-
putInt("total", totalCount); putInt("index", idx); putMap("image", copy)
|
|
1032
|
+
emitGalleryEvent("onProcessingStart", Arguments.createMap().apply {
|
|
1143
1033
|
putString("processingId", processingId)
|
|
1034
|
+
putInt("total", totalCount)
|
|
1035
|
+
putArray("items", arr)
|
|
1144
1036
|
})
|
|
1145
|
-
} catch (_: Exception) {}
|
|
1146
|
-
currentIndex++
|
|
1147
|
-
continue
|
|
1148
|
-
}
|
|
1149
|
-
try {
|
|
1150
|
-
// 1. Get EXIF Orientation
|
|
1151
|
-
var orientation = android.media.ExifInterface.ORIENTATION_NORMAL
|
|
1152
|
-
try {
|
|
1153
|
-
contentResolver.openInputStream(uri)?.use { exifInputStream ->
|
|
1154
|
-
val exif = android.media.ExifInterface(exifInputStream)
|
|
1155
|
-
orientation = exif.getAttributeInt(android.media.ExifInterface.TAG_ORIENTATION, android.media.ExifInterface.ORIENTATION_NORMAL)
|
|
1156
1037
|
}
|
|
1157
|
-
} catch (e: Exception) {}
|
|
1158
1038
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
var originalWidth = originalOptions.outWidth
|
|
1167
|
-
var originalHeight = originalOptions.outHeight
|
|
1168
|
-
|
|
1169
|
-
// Swap width and height if rotated by 90 or 270 degrees
|
|
1170
|
-
if (orientation == android.media.ExifInterface.ORIENTATION_ROTATE_90 || orientation == android.media.ExifInterface.ORIENTATION_ROTATE_270) {
|
|
1171
|
-
val temp = originalWidth
|
|
1172
|
-
originalWidth = originalHeight
|
|
1173
|
-
originalHeight = temp
|
|
1039
|
+
override fun onVideoProgress(index: Int, progress: Float) {
|
|
1040
|
+
hud?.let { h -> main.post { h.update(progress, index, totalCount) } }
|
|
1041
|
+
emitGalleryEvent("onVideoProgress", Arguments.createMap().apply {
|
|
1042
|
+
putInt("index", index); putInt("total", totalCount); putDouble("progress", progress.toDouble())
|
|
1043
|
+
putString("processingId", processingId)
|
|
1044
|
+
})
|
|
1174
1045
|
}
|
|
1175
|
-
|
|
1176
|
-
// Load and resize image
|
|
1177
|
-
contentResolver.openInputStream(uri)?.use { resizedInputStream ->
|
|
1178
|
-
// Initial decode for sizing
|
|
1179
|
-
val decodeOptions = BitmapFactory.Options().apply {
|
|
1180
|
-
inSampleSize = calculateInSampleSize(originalWidth, originalHeight, maxWidth, maxHeight)
|
|
1181
|
-
}
|
|
1182
|
-
var bitmap = BitmapFactory.decodeStream(resizedInputStream, null, decodeOptions)
|
|
1183
|
-
|
|
1184
|
-
if (bitmap != null) {
|
|
1185
|
-
// Apply EXIF Rotation
|
|
1186
|
-
val matrix = android.graphics.Matrix()
|
|
1187
|
-
when (orientation) {
|
|
1188
|
-
android.media.ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
|
|
1189
|
-
android.media.ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
|
|
1190
|
-
android.media.ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
|
|
1191
|
-
android.media.ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1f, 1f)
|
|
1192
|
-
android.media.ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1f, -1f)
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
if (!matrix.isIdentity) {
|
|
1196
|
-
val rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
1197
|
-
if (rotatedBitmap != bitmap) {
|
|
1198
|
-
bitmap.recycle()
|
|
1199
|
-
bitmap = rotatedBitmap
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// Precise resize if needed
|
|
1204
|
-
val finalBitmap = if (bitmap.width > maxWidth || bitmap.height > maxHeight) {
|
|
1205
|
-
resizeBitmap(bitmap, maxWidth, maxHeight)
|
|
1206
|
-
} else {
|
|
1207
|
-
bitmap
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
val processedBitmap = ImageProcessor.applyWatermarkIfNeeded(reactApplicationContext, finalBitmap)
|
|
1211
|
-
|
|
1212
|
-
// Determine format and extension
|
|
1213
|
-
val compressFormat: Bitmap.CompressFormat
|
|
1214
|
-
val extension: String
|
|
1215
|
-
val mimeType: String
|
|
1216
|
-
|
|
1217
|
-
when (outputFormat) {
|
|
1218
|
-
"png" -> {
|
|
1219
|
-
compressFormat = Bitmap.CompressFormat.PNG
|
|
1220
|
-
extension = "png"
|
|
1221
|
-
mimeType = "image/png"
|
|
1222
|
-
}
|
|
1223
|
-
"jpg", "jpeg" -> {
|
|
1224
|
-
compressFormat = Bitmap.CompressFormat.JPEG
|
|
1225
|
-
extension = "jpg"
|
|
1226
|
-
mimeType = "image/jpeg"
|
|
1227
|
-
}
|
|
1228
|
-
else -> { // webp
|
|
1229
|
-
compressFormat = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
1230
|
-
Bitmap.CompressFormat.WEBP_LOSSY
|
|
1231
|
-
} else {
|
|
1232
|
-
@Suppress("DEPRECATION")
|
|
1233
|
-
Bitmap.CompressFormat.WEBP
|
|
1234
|
-
}
|
|
1235
|
-
extension = "webp"
|
|
1236
|
-
mimeType = "image/webp"
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
1046
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
val finalWidth = processedBitmap.width
|
|
1251
|
-
val finalHeight = processedBitmap.height
|
|
1252
|
-
|
|
1253
|
-
// Cleanup
|
|
1254
|
-
if (bitmap != processedBitmap) {
|
|
1255
|
-
bitmap.recycle()
|
|
1256
|
-
}
|
|
1257
|
-
if (finalBitmap != processedBitmap && finalBitmap != bitmap) {
|
|
1258
|
-
finalBitmap.recycle()
|
|
1259
|
-
}
|
|
1260
|
-
processedBitmap.recycle()
|
|
1261
|
-
|
|
1262
|
-
val createImageInfo = {
|
|
1263
|
-
WritableNativeMap().apply {
|
|
1264
|
-
putString("uri", Uri.fromFile(tempFile).toString())
|
|
1265
|
-
putInt("width", finalWidth)
|
|
1266
|
-
putInt("height", finalHeight)
|
|
1267
|
-
putString("type", mimeType)
|
|
1268
|
-
putString("fileName", tempFile.name)
|
|
1269
|
-
putString("fileExtension", extension)
|
|
1270
|
-
putDouble("fileSize", tempFile.length().toDouble())
|
|
1271
|
-
putString("originalUri", uri.toString())
|
|
1272
|
-
putInt("originalWidth", originalWidth)
|
|
1273
|
-
putInt("originalHeight", originalHeight)
|
|
1274
|
-
if (thumb != null) {
|
|
1275
|
-
putString("thumbnailUri", Uri.fromFile(thumb.file).toString())
|
|
1276
|
-
putInt("thumbnailWidth", thumb.width)
|
|
1277
|
-
putInt("thumbnailHeight", thumb.height)
|
|
1278
|
-
putDouble("thumbnailFileSize", thumb.fileSize.toDouble())
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
val imageInfo = createImageInfo()
|
|
1283
|
-
results.add(imageInfo)
|
|
1284
|
-
|
|
1285
|
-
try {
|
|
1286
|
-
val eventMap = WritableNativeMap().apply {
|
|
1287
|
-
putInt("total", totalCount)
|
|
1288
|
-
putInt("index", currentIndex)
|
|
1289
|
-
putString("processingId", processingId)
|
|
1290
|
-
putMap("image", createImageInfo())
|
|
1291
|
-
}
|
|
1292
|
-
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
1293
|
-
.emit("onImageProcessed", eventMap)
|
|
1294
|
-
} catch (e: Exception) { }
|
|
1295
|
-
}
|
|
1047
|
+
override fun onItem(result: com.rnattachmentkit.AttachmentProcessor.Result) {
|
|
1048
|
+
if (result.isVideo) hud?.let { h -> main.post { h.update(1f, result.index, totalCount) } }
|
|
1049
|
+
try {
|
|
1050
|
+
emitter().emit("onImageProcessed", WritableNativeMap().apply {
|
|
1051
|
+
putInt("total", totalCount); putInt("index", result.index); putMap("image", toWritableMap(result))
|
|
1052
|
+
putString("processingId", processingId)
|
|
1053
|
+
})
|
|
1054
|
+
} catch (_: Exception) {}
|
|
1296
1055
|
}
|
|
1297
|
-
}
|
|
1298
|
-
currentIndex++
|
|
1299
|
-
} catch (e: Exception) {
|
|
1300
|
-
currentIndex++
|
|
1301
|
-
// Skip failed images
|
|
1302
|
-
continue
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
} catch (e: com.rnattachmentkit.VideoExporter.CancelledException) {
|
|
1306
|
-
// 취소: 지금까지 만든 동영상 파일은 지움
|
|
1307
|
-
videoFiles.forEach { try { it.delete() } catch (_: Exception) {} }
|
|
1308
|
-
throw e
|
|
1056
|
+
}).map { toWritableMap(it) }
|
|
1309
1057
|
} finally {
|
|
1310
1058
|
activeProcessingJobs.remove(processingId)
|
|
1311
1059
|
if (hud != null) withContext(Dispatchers.Main) { hud?.dismiss() }
|
|
1312
1060
|
}
|
|
1313
|
-
|
|
1314
|
-
results
|
|
1315
1061
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
if (reqHeight > maxHeight || reqWidth > maxWidth) {
|
|
1326
|
-
val halfHeight = reqHeight / 2
|
|
1327
|
-
val halfWidth = reqWidth / 2
|
|
1328
|
-
|
|
1329
|
-
while ((halfHeight / inSampleSize) >= maxHeight && (halfWidth / inSampleSize) >= maxWidth) {
|
|
1330
|
-
inSampleSize *= 2
|
|
1062
|
+
|
|
1063
|
+
/** AttachmentProcessor.Result → RN 결과 맵 (예전 브릿지와 같은 필드·타입) */
|
|
1064
|
+
private fun toWritableMap(r: com.rnattachmentkit.AttachmentProcessor.Result): WritableMap = WritableNativeMap().apply {
|
|
1065
|
+
r.toMap().forEach { (k, v) ->
|
|
1066
|
+
when (v) {
|
|
1067
|
+
is Boolean -> putBoolean(k, v)
|
|
1068
|
+
is Int -> putInt(k, v)
|
|
1069
|
+
is Number -> putDouble(k, v.toDouble())
|
|
1070
|
+
else -> putString(k, v.toString())
|
|
1331
1071
|
}
|
|
1332
1072
|
}
|
|
1333
|
-
|
|
1334
|
-
return inSampleSize
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
private fun resizeBitmap(bitmap: Bitmap, maxWidth: Int, maxHeight: Int): Bitmap {
|
|
1338
|
-
val width = bitmap.width
|
|
1339
|
-
val height = bitmap.height
|
|
1340
|
-
|
|
1341
|
-
val ratioBitmap = width.toFloat() / height.toFloat()
|
|
1342
|
-
val ratioMax = maxWidth.toFloat() / maxHeight.toFloat()
|
|
1343
|
-
|
|
1344
|
-
var finalWidth = maxWidth
|
|
1345
|
-
var finalHeight = maxHeight
|
|
1346
|
-
|
|
1347
|
-
if (ratioMax > ratioBitmap) {
|
|
1348
|
-
finalWidth = (maxHeight * ratioBitmap).toInt()
|
|
1349
|
-
} else {
|
|
1350
|
-
finalHeight = (maxWidth / ratioBitmap).toInt()
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
return Bitmap.createScaledBitmap(bitmap, finalWidth, finalHeight, true)
|
|
1354
1073
|
}
|
|
1355
1074
|
|
|
1356
1075
|
|