@developer_tribe/react-native-comnyx 0.13.13 → 0.15.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/android/generated/RCTAppDependencyProvider.h +25 -0
- package/android/generated/RCTAppDependencyProvider.mm +55 -0
- package/android/generated/RCTModulesConformingToProtocolsProvider.h +18 -0
- package/android/generated/RCTModulesConformingToProtocolsProvider.mm +33 -0
- package/android/generated/RCTThirdPartyComponentsProvider.h +16 -0
- package/android/generated/RCTThirdPartyComponentsProvider.mm +23 -0
- package/android/generated/ReactAppDependencyProvider.podspec +34 -0
- package/android/generated/jni/CMakeLists.txt +36 -0
- package/android/generated/jni/RNComnyxSpec-generated.cpp +22 -0
- package/android/generated/jni/RNComnyxSpec.h +24 -0
- package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI-generated.cpp +17 -0
- package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI.h +19 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/AndroidManifestNew.xml +4 -0
- package/android/src/main/java/com/comnyx/ComnyxMediaPickerModule.kt +347 -0
- package/android/src/main/java/com/comnyx/ComnyxPackage.kt +1 -1
- package/android/src/main/java/com/comnyx/VideoPlayerActivity.kt +91 -0
- package/ios/ComnyxMediaPicker.m +29 -0
- package/ios/ComnyxMediaPicker.swift +436 -0
- package/lib/commonjs/NativeComnyxMediaPicker.js +83 -0
- package/lib/commonjs/NativeComnyxMediaPicker.js.map +1 -0
- package/lib/commonjs/api/index.js +19 -0
- package/lib/commonjs/api/index.js.map +1 -1
- package/lib/commonjs/api/media.js +76 -0
- package/lib/commonjs/api/media.js.map +1 -0
- package/lib/commonjs/assets/attachment-01.png +0 -0
- package/lib/commonjs/assets/gallery.png +0 -0
- package/lib/commonjs/assets/video-play.png +0 -0
- package/lib/commonjs/assets/x-circle.png +0 -0
- package/lib/commonjs/components/ChatList.js +48 -22
- package/lib/commonjs/components/ChatList.js.map +1 -1
- package/lib/commonjs/components/LinkifyText.js +5 -1
- package/lib/commonjs/components/LinkifyText.js.map +1 -1
- package/lib/commonjs/components/MediaMessageItem.js +333 -0
- package/lib/commonjs/components/MediaMessageItem.js.map +1 -0
- package/lib/commonjs/components/MediaPickerButton.js +244 -0
- package/lib/commonjs/components/MediaPickerButton.js.map +1 -0
- package/lib/commonjs/components/MediaViewerModal.js +164 -0
- package/lib/commonjs/components/MediaViewerModal.js.map +1 -0
- package/lib/commonjs/components/MessageInput.js +344 -73
- package/lib/commonjs/components/MessageInput.js.map +1 -1
- package/lib/commonjs/components/MessageItem.js +17 -8
- package/lib/commonjs/components/MessageItem.js.map +1 -1
- package/lib/commonjs/constants/translations.js +174 -29
- package/lib/commonjs/constants/translations.js.map +1 -1
- package/lib/commonjs/data/fake/media.js +105 -0
- package/lib/commonjs/data/fake/media.js.map +1 -0
- package/lib/commonjs/types/MediaTypes.js +2 -0
- package/lib/commonjs/types/MediaTypes.js.map +1 -0
- package/lib/commonjs/version.js +1 -1
- package/lib/commonjs/version.js.map +1 -1
- package/lib/module/NativeComnyxMediaPicker.js +73 -0
- package/lib/module/NativeComnyxMediaPicker.js.map +1 -0
- package/lib/module/api/index.js +1 -0
- package/lib/module/api/index.js.map +1 -1
- package/lib/module/api/media.js +70 -0
- package/lib/module/api/media.js.map +1 -0
- package/lib/module/assets/attachment-01.png +0 -0
- package/lib/module/assets/gallery.png +0 -0
- package/lib/module/assets/video-play.png +0 -0
- package/lib/module/assets/x-circle.png +0 -0
- package/lib/module/components/ChatList.js +48 -22
- package/lib/module/components/ChatList.js.map +1 -1
- package/lib/module/components/LinkifyText.js +5 -1
- package/lib/module/components/LinkifyText.js.map +1 -1
- package/lib/module/components/MediaMessageItem.js +330 -0
- package/lib/module/components/MediaMessageItem.js.map +1 -0
- package/lib/module/components/MediaPickerButton.js +240 -0
- package/lib/module/components/MediaPickerButton.js.map +1 -0
- package/lib/module/components/MediaViewerModal.js +160 -0
- package/lib/module/components/MediaViewerModal.js.map +1 -0
- package/lib/module/components/MessageInput.js +347 -75
- package/lib/module/components/MessageInput.js.map +1 -1
- package/lib/module/components/MessageItem.js +17 -8
- package/lib/module/components/MessageItem.js.map +1 -1
- package/lib/module/constants/translations.js +174 -29
- package/lib/module/constants/translations.js.map +1 -1
- package/lib/module/data/fake/media.js +99 -0
- package/lib/module/data/fake/media.js.map +1 -0
- package/lib/module/types/MediaTypes.js +2 -0
- package/lib/module/types/MediaTypes.js.map +1 -0
- package/lib/module/version.js +1 -1
- package/lib/module/version.js.map +1 -1
- package/lib/typescript/src/NativeComnyxMediaPicker.d.ts +9 -0
- package/lib/typescript/src/NativeComnyxMediaPicker.d.ts.map +1 -0
- package/lib/typescript/src/api/index.d.ts +1 -0
- package/lib/typescript/src/api/index.d.ts.map +1 -1
- package/lib/typescript/src/api/media.d.ts +7 -0
- package/lib/typescript/src/api/media.d.ts.map +1 -0
- package/lib/typescript/src/components/ChatList.d.ts.map +1 -1
- package/lib/typescript/src/components/LinkifyText.d.ts.map +1 -1
- package/lib/typescript/src/components/MediaMessageItem.d.ts +6 -0
- package/lib/typescript/src/components/MediaMessageItem.d.ts.map +1 -0
- package/lib/typescript/src/components/MediaPickerButton.d.ts +5 -0
- package/lib/typescript/src/components/MediaPickerButton.d.ts.map +1 -0
- package/lib/typescript/src/components/MediaViewerModal.d.ts +8 -0
- package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -0
- package/lib/typescript/src/components/MessageInput.d.ts.map +1 -1
- package/lib/typescript/src/components/MessageItem.d.ts.map +1 -1
- package/lib/typescript/src/constants/translations.d.ts.map +1 -1
- package/lib/typescript/src/data/fake/media.d.ts +6 -0
- package/lib/typescript/src/data/fake/media.d.ts.map +1 -0
- package/lib/typescript/src/types/Conversation.d.ts +19 -0
- package/lib/typescript/src/types/Conversation.d.ts.map +1 -1
- package/lib/typescript/src/types/LocalizationKeys.d.ts +5 -0
- package/lib/typescript/src/types/LocalizationKeys.d.ts.map +1 -1
- package/lib/typescript/src/types/MediaTypes.d.ts +26 -0
- package/lib/typescript/src/types/MediaTypes.d.ts.map +1 -0
- package/lib/typescript/src/version.d.ts +1 -1
- package/lib/typescript/src/version.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeComnyxMediaPicker.ts +83 -0
- package/src/api/index.ts +1 -0
- package/src/api/media.ts +116 -0
- package/src/assets/attachment-01.png +0 -0
- package/src/assets/gallery.png +0 -0
- package/src/assets/video-play.png +0 -0
- package/src/assets/x-circle.png +0 -0
- package/src/components/ChatList.tsx +81 -24
- package/src/components/CustomerForm.tsx +1 -1
- package/src/components/LinkifyText.tsx +3 -2
- package/src/components/MediaMessageItem.tsx +390 -0
- package/src/components/MediaPickerButton.tsx +269 -0
- package/src/components/MediaViewerModal.tsx +168 -0
- package/src/components/MessageInput.tsx +396 -84
- package/src/components/MessageItem.tsx +19 -5
- package/src/constants/translations.ts +145 -0
- package/src/data/fake/media.ts +110 -0
- package/src/types/Conversation.ts +20 -0
- package/src/types/LocalizationKeys.ts +5 -0
- package/src/types/MediaTypes.ts +27 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
package com.comnyx
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.graphics.Bitmap
|
|
6
|
+
import android.graphics.BitmapFactory
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import android.provider.OpenableColumns
|
|
9
|
+
import android.webkit.MimeTypeMap
|
|
10
|
+
import com.facebook.react.bridge.*
|
|
11
|
+
import java.io.File
|
|
12
|
+
import java.io.FileOutputStream
|
|
13
|
+
import java.io.InputStream
|
|
14
|
+
import android.media.MediaMetadataRetriever
|
|
15
|
+
|
|
16
|
+
class ComnyxMediaPickerModule(reactContext: ReactApplicationContext) :
|
|
17
|
+
ReactContextBaseJavaModule(reactContext), ActivityEventListener {
|
|
18
|
+
|
|
19
|
+
companion object {
|
|
20
|
+
const val NAME = "ComnyxMediaPicker"
|
|
21
|
+
private const val REQUEST_MEDIA = 2003
|
|
22
|
+
private const val REQUEST_IMAGE = 2004
|
|
23
|
+
private const val REQUEST_VIDEO = 2005
|
|
24
|
+
private const val COMPRESS_THRESHOLD = 500 * 1024 // 500KB
|
|
25
|
+
private const val COMPRESS_QUALITY = 80
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private var promise: Promise? = null
|
|
29
|
+
|
|
30
|
+
private fun rejectPendingPromise() {
|
|
31
|
+
promise?.reject("CANCELLED", "A new picker request was made before the previous one completed")
|
|
32
|
+
promise = null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
init {
|
|
36
|
+
reactContext.addActivityEventListener(this)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun getName(): String = NAME
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
// MARK: - Pick Media (both images and videos)
|
|
44
|
+
|
|
45
|
+
@ReactMethod
|
|
46
|
+
fun pickMedia(promise: Promise) {
|
|
47
|
+
rejectPendingPromise()
|
|
48
|
+
this.promise = promise
|
|
49
|
+
val activity = reactApplicationContext.currentActivity
|
|
50
|
+
if (activity == null) {
|
|
51
|
+
promise.reject("NO_ACTIVITY", "Activity is not available")
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
55
|
+
type = "*/*"
|
|
56
|
+
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
|
|
57
|
+
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
|
58
|
+
addCategory(Intent.CATEGORY_OPENABLE)
|
|
59
|
+
}
|
|
60
|
+
activity.startActivityForResult(intent, REQUEST_MEDIA)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// MARK: - Pick Image Only
|
|
64
|
+
|
|
65
|
+
@ReactMethod
|
|
66
|
+
fun pickImage(promise: Promise) {
|
|
67
|
+
rejectPendingPromise()
|
|
68
|
+
this.promise = promise
|
|
69
|
+
val activity = reactApplicationContext.currentActivity
|
|
70
|
+
if (activity == null) {
|
|
71
|
+
promise.reject("NO_ACTIVITY", "Activity is not available")
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
75
|
+
type = "image/*"
|
|
76
|
+
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
|
77
|
+
addCategory(Intent.CATEGORY_OPENABLE)
|
|
78
|
+
}
|
|
79
|
+
activity.startActivityForResult(intent, REQUEST_IMAGE)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// MARK: - Pick Video Only
|
|
83
|
+
|
|
84
|
+
@ReactMethod
|
|
85
|
+
fun pickVideo(promise: Promise) {
|
|
86
|
+
rejectPendingPromise()
|
|
87
|
+
this.promise = promise
|
|
88
|
+
val activity = reactApplicationContext.currentActivity
|
|
89
|
+
if (activity == null) {
|
|
90
|
+
promise.reject("NO_ACTIVITY", "Activity is not available")
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
94
|
+
type = "video/*"
|
|
95
|
+
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
|
96
|
+
addCategory(Intent.CATEGORY_OPENABLE)
|
|
97
|
+
}
|
|
98
|
+
activity.startActivityForResult(intent, REQUEST_VIDEO)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// MARK: - Generate Thumbnail from Video URL
|
|
102
|
+
|
|
103
|
+
@ReactMethod
|
|
104
|
+
fun generateThumbnail(videoUrl: String, promise: Promise) {
|
|
105
|
+
android.util.Log.d("ComnyxMedia", "generateThumbnail called with: $videoUrl")
|
|
106
|
+
Thread {
|
|
107
|
+
try {
|
|
108
|
+
val retriever = MediaMetadataRetriever()
|
|
109
|
+
if (videoUrl.startsWith("http://") || videoUrl.startsWith("https://")) {
|
|
110
|
+
android.util.Log.d("ComnyxMedia", "Setting remote data source")
|
|
111
|
+
retriever.setDataSource(videoUrl, HashMap<String, String>())
|
|
112
|
+
} else {
|
|
113
|
+
android.util.Log.d("ComnyxMedia", "Setting local data source")
|
|
114
|
+
retriever.setDataSource(reactApplicationContext, Uri.parse(videoUrl))
|
|
115
|
+
}
|
|
116
|
+
val bitmap = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
|
117
|
+
retriever.release()
|
|
118
|
+
|
|
119
|
+
if (bitmap != null) {
|
|
120
|
+
android.util.Log.d("ComnyxMedia", "Thumbnail bitmap generated: ${bitmap.width}x${bitmap.height}")
|
|
121
|
+
val thumbFile = File(reactApplicationContext.cacheDir, "thumb_${System.currentTimeMillis()}.jpg")
|
|
122
|
+
FileOutputStream(thumbFile).use { fos ->
|
|
123
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos)
|
|
124
|
+
}
|
|
125
|
+
bitmap.recycle()
|
|
126
|
+
promise.resolve("file://" + thumbFile.absolutePath)
|
|
127
|
+
} else {
|
|
128
|
+
android.util.Log.d("ComnyxMedia", "Thumbnail bitmap is null")
|
|
129
|
+
promise.resolve(null)
|
|
130
|
+
}
|
|
131
|
+
} catch (e: Exception) {
|
|
132
|
+
android.util.Log.e("ComnyxMedia", "generateThumbnail error: ${e.message}", e)
|
|
133
|
+
promise.resolve(null)
|
|
134
|
+
}
|
|
135
|
+
}.start()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// MARK: - Open Video
|
|
139
|
+
|
|
140
|
+
@ReactMethod
|
|
141
|
+
fun openVideo(uri: String, promise: Promise) {
|
|
142
|
+
try {
|
|
143
|
+
val activity = reactApplicationContext.currentActivity
|
|
144
|
+
if (activity == null) {
|
|
145
|
+
promise.reject("NO_ACTIVITY", "Activity is not available")
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (uri.startsWith("http://") || uri.startsWith("https://")) {
|
|
150
|
+
// Remote URL — use system player for faster streaming
|
|
151
|
+
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
152
|
+
setDataAndType(Uri.parse(uri), "video/*")
|
|
153
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
154
|
+
}
|
|
155
|
+
activity.startActivity(intent)
|
|
156
|
+
promise.resolve(null)
|
|
157
|
+
} else {
|
|
158
|
+
// Local file — use custom VideoPlayerActivity
|
|
159
|
+
val intent = Intent(activity, VideoPlayerActivity::class.java).apply {
|
|
160
|
+
putExtra("video_uri", uri)
|
|
161
|
+
}
|
|
162
|
+
activity.startActivity(intent)
|
|
163
|
+
promise.resolve(null)
|
|
164
|
+
}
|
|
165
|
+
} catch (e: Exception) {
|
|
166
|
+
promise.reject("OPEN_ERROR", e.message, e)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// MARK: - Activity Result
|
|
171
|
+
|
|
172
|
+
override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) {
|
|
173
|
+
if (requestCode != REQUEST_MEDIA && requestCode != REQUEST_IMAGE && requestCode != REQUEST_VIDEO) return
|
|
174
|
+
|
|
175
|
+
if (resultCode != Activity.RESULT_OK || data == null) {
|
|
176
|
+
promise?.resolve(null)
|
|
177
|
+
promise = null
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Collect all selected URIs
|
|
182
|
+
val uris = mutableListOf<Uri>()
|
|
183
|
+
val clipData = data.clipData
|
|
184
|
+
if (clipData != null) {
|
|
185
|
+
for (i in 0 until clipData.itemCount) {
|
|
186
|
+
uris.add(clipData.getItemAt(i).uri)
|
|
187
|
+
}
|
|
188
|
+
} else if (data.data != null) {
|
|
189
|
+
uris.add(data.data!!)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (uris.isEmpty()) {
|
|
193
|
+
promise?.resolve(null)
|
|
194
|
+
promise = null
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Capture and clear promise to prevent double-resolve races
|
|
199
|
+
val currentPromise = promise
|
|
200
|
+
promise = null
|
|
201
|
+
|
|
202
|
+
// Move all heavy I/O to a background thread to prevent ANR
|
|
203
|
+
Thread {
|
|
204
|
+
try {
|
|
205
|
+
val context = reactApplicationContext
|
|
206
|
+
val contentResolver = context.contentResolver
|
|
207
|
+
val resultsArray = Arguments.createArray()
|
|
208
|
+
|
|
209
|
+
for (uri in uris) {
|
|
210
|
+
val mimeTypeStr = contentResolver.getType(uri) ?: ""
|
|
211
|
+
val isImage = mimeTypeStr.startsWith("image")
|
|
212
|
+
val mimeType = contentResolver.getType(uri) ?: if (isImage) "image/jpeg" else "video/mp4"
|
|
213
|
+
|
|
214
|
+
// Get file name
|
|
215
|
+
var fileName = "file_${System.currentTimeMillis()}"
|
|
216
|
+
val cursor = contentResolver.query(uri, null, null, null, null)
|
|
217
|
+
cursor?.use {
|
|
218
|
+
if (it.moveToFirst()) {
|
|
219
|
+
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
|
220
|
+
if (nameIndex >= 0) {
|
|
221
|
+
fileName = it.getString(nameIndex) ?: fileName
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Copy to temp file
|
|
227
|
+
val tempFile = File(context.cacheDir, "${System.currentTimeMillis()}_$fileName")
|
|
228
|
+
contentResolver.openInputStream(uri)?.use { inputStream ->
|
|
229
|
+
FileOutputStream(tempFile).use { outputStream ->
|
|
230
|
+
inputStream.copyTo(outputStream)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
var finalFile = tempFile
|
|
235
|
+
var finalMimeType = mimeType
|
|
236
|
+
var finalFileSize = tempFile.length()
|
|
237
|
+
|
|
238
|
+
// Compress image if >500KB
|
|
239
|
+
if (isImage && finalFileSize > COMPRESS_THRESHOLD) {
|
|
240
|
+
val compressed = compressImage(tempFile)
|
|
241
|
+
if (compressed != null) {
|
|
242
|
+
tempFile.delete()
|
|
243
|
+
finalFile = compressed
|
|
244
|
+
finalMimeType = "image/jpeg"
|
|
245
|
+
finalFileSize = compressed.length()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
val result = Arguments.createMap().apply {
|
|
250
|
+
putString("uri", Uri.fromFile(finalFile).toString())
|
|
251
|
+
putString("type", if (isImage) "image" else "video")
|
|
252
|
+
putString("fileName", finalFile.name)
|
|
253
|
+
putDouble("fileSize", finalFileSize.toDouble())
|
|
254
|
+
putString("mimeType", finalMimeType)
|
|
255
|
+
if (!isImage) {
|
|
256
|
+
val thumbnailUri = generateVideoThumbnail(finalFile)
|
|
257
|
+
if (thumbnailUri != null) {
|
|
258
|
+
putString("thumbnailUri", thumbnailUri)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
resultsArray.pushMap(result)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
currentPromise?.resolve(resultsArray)
|
|
266
|
+
} catch (e: Exception) {
|
|
267
|
+
currentPromise?.reject("PICK_ERROR", e.message, e)
|
|
268
|
+
}
|
|
269
|
+
}.start()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
override fun onNewIntent(intent: Intent) {
|
|
273
|
+
// Not needed
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// MARK: - Image Compression
|
|
277
|
+
|
|
278
|
+
private fun compressImage(file: File): File? {
|
|
279
|
+
return try {
|
|
280
|
+
val bitmap = BitmapFactory.decodeFile(file.absolutePath) ?: return null
|
|
281
|
+
val compressedFile = File(reactApplicationContext.cacheDir, "compressed_${System.currentTimeMillis()}.jpg")
|
|
282
|
+
FileOutputStream(compressedFile).use { outputStream ->
|
|
283
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, COMPRESS_QUALITY, outputStream)
|
|
284
|
+
}
|
|
285
|
+
bitmap.recycle()
|
|
286
|
+
compressedFile
|
|
287
|
+
} catch (e: Exception) {
|
|
288
|
+
null
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private fun generateVideoThumbnail(videoFile: File): String? {
|
|
293
|
+
return try {
|
|
294
|
+
val retriever = MediaMetadataRetriever()
|
|
295
|
+
retriever.setDataSource(videoFile.absolutePath)
|
|
296
|
+
val bitmap = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
|
297
|
+
retriever.release()
|
|
298
|
+
if (bitmap != null) {
|
|
299
|
+
val thumbFile = File(reactApplicationContext.cacheDir, "thumb_${System.currentTimeMillis()}.jpg")
|
|
300
|
+
FileOutputStream(thumbFile).use { outputStream ->
|
|
301
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, 70, outputStream)
|
|
302
|
+
}
|
|
303
|
+
bitmap.recycle()
|
|
304
|
+
Uri.fromFile(thumbFile).toString()
|
|
305
|
+
} else null
|
|
306
|
+
} catch (e: Exception) {
|
|
307
|
+
null
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
@ReactMethod
|
|
312
|
+
fun deleteTempFile(uri: String, promise: Promise) {
|
|
313
|
+
Thread {
|
|
314
|
+
try {
|
|
315
|
+
val path = uri.removePrefix("file://")
|
|
316
|
+
val file = File(path)
|
|
317
|
+
if (file.exists()) {
|
|
318
|
+
file.delete()
|
|
319
|
+
}
|
|
320
|
+
promise.resolve(null)
|
|
321
|
+
} catch (e: Exception) {
|
|
322
|
+
promise.resolve(null)
|
|
323
|
+
}
|
|
324
|
+
}.start()
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@ReactMethod
|
|
328
|
+
fun cleanupTempFiles(promise: Promise) {
|
|
329
|
+
Thread {
|
|
330
|
+
try {
|
|
331
|
+
val cacheDir = reactApplicationContext.cacheDir
|
|
332
|
+
val files = cacheDir.listFiles() ?: emptyArray()
|
|
333
|
+
for (file in files) {
|
|
334
|
+
val name = file.name.lowercase()
|
|
335
|
+
if (name.startsWith("thumb_") || name.startsWith("compressed_") ||
|
|
336
|
+
(name.contains("_") && (name.endsWith(".jpg") || name.endsWith(".jpeg") ||
|
|
337
|
+
name.endsWith(".png") || name.endsWith(".mp4") || name.endsWith(".mov")))) {
|
|
338
|
+
file.delete()
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
promise.resolve(null)
|
|
342
|
+
} catch (e: Exception) {
|
|
343
|
+
promise.resolve(null)
|
|
344
|
+
}
|
|
345
|
+
}.start()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -7,7 +7,7 @@ import com.facebook.react.uimanager.ViewManager
|
|
|
7
7
|
|
|
8
8
|
class ComnyxPackage : ReactPackage {
|
|
9
9
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
10
|
-
return listOf(ComnyxModule(reactContext))
|
|
10
|
+
return listOf(ComnyxModule(reactContext), ComnyxMediaPickerModule(reactContext))
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package com.comnyx
|
|
2
|
+
|
|
3
|
+
import android.net.Uri
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import android.view.Gravity
|
|
6
|
+
import android.view.Window
|
|
7
|
+
import android.view.WindowManager
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
import android.widget.ImageButton
|
|
10
|
+
import android.widget.ProgressBar
|
|
11
|
+
import android.widget.VideoView
|
|
12
|
+
import android.app.Activity as AndroidActivity
|
|
13
|
+
import android.widget.MediaController
|
|
14
|
+
|
|
15
|
+
class VideoPlayerActivity : AndroidActivity() {
|
|
16
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
17
|
+
super.onCreate(savedInstanceState)
|
|
18
|
+
|
|
19
|
+
// Full screen
|
|
20
|
+
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
|
21
|
+
window.setFlags(
|
|
22
|
+
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
|
23
|
+
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
val container = FrameLayout(this)
|
|
27
|
+
container.setBackgroundColor(android.graphics.Color.BLACK)
|
|
28
|
+
|
|
29
|
+
val videoView = VideoView(this)
|
|
30
|
+
val videoParams = FrameLayout.LayoutParams(
|
|
31
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
32
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
33
|
+
Gravity.CENTER
|
|
34
|
+
)
|
|
35
|
+
container.addView(videoView, videoParams)
|
|
36
|
+
|
|
37
|
+
// Loading spinner
|
|
38
|
+
val progressBar = ProgressBar(this)
|
|
39
|
+
val progressParams = FrameLayout.LayoutParams(
|
|
40
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
41
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
42
|
+
Gravity.CENTER
|
|
43
|
+
)
|
|
44
|
+
container.addView(progressBar, progressParams)
|
|
45
|
+
|
|
46
|
+
// Close button (X) top-right
|
|
47
|
+
val closeButton = ImageButton(this)
|
|
48
|
+
closeButton.setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
|
49
|
+
closeButton.setImageResource(android.R.drawable.ic_menu_close_clear_cancel)
|
|
50
|
+
closeButton.setColorFilter(android.graphics.Color.WHITE)
|
|
51
|
+
closeButton.setPadding(24, 24, 24, 24)
|
|
52
|
+
val closeParams = FrameLayout.LayoutParams(
|
|
53
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
54
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
55
|
+
Gravity.TOP or Gravity.END
|
|
56
|
+
)
|
|
57
|
+
closeParams.topMargin = 60
|
|
58
|
+
closeParams.rightMargin = 24
|
|
59
|
+
container.addView(closeButton, closeParams)
|
|
60
|
+
|
|
61
|
+
closeButton.setOnClickListener {
|
|
62
|
+
finish()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setContentView(container)
|
|
66
|
+
|
|
67
|
+
val videoUri = intent.getStringExtra("video_uri") ?: run {
|
|
68
|
+
finish()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// MediaController for play/pause/seek controls
|
|
73
|
+
val mediaController = MediaController(this)
|
|
74
|
+
mediaController.setAnchorView(videoView)
|
|
75
|
+
videoView.setMediaController(mediaController)
|
|
76
|
+
|
|
77
|
+
videoView.setVideoURI(Uri.parse(videoUri))
|
|
78
|
+
videoView.setOnPreparedListener { mp ->
|
|
79
|
+
progressBar.visibility = android.view.View.GONE
|
|
80
|
+
mp.isLooping = false
|
|
81
|
+
videoView.start()
|
|
82
|
+
}
|
|
83
|
+
videoView.setOnCompletionListener {
|
|
84
|
+
finish()
|
|
85
|
+
}
|
|
86
|
+
videoView.setOnErrorListener { _, _, _ ->
|
|
87
|
+
finish()
|
|
88
|
+
true
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
@interface RCT_EXTERN_MODULE(ComnyxMediaPicker, NSObject)
|
|
4
|
+
|
|
5
|
+
RCT_EXTERN_METHOD(pickMedia:(RCTPromiseResolveBlock)resolve
|
|
6
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
7
|
+
|
|
8
|
+
RCT_EXTERN_METHOD(pickImage:(RCTPromiseResolveBlock)resolve
|
|
9
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
10
|
+
|
|
11
|
+
RCT_EXTERN_METHOD(pickVideo:(RCTPromiseResolveBlock)resolve
|
|
12
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
13
|
+
|
|
14
|
+
RCT_EXTERN_METHOD(generateThumbnail:(NSString *)videoUrl
|
|
15
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
16
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
17
|
+
|
|
18
|
+
RCT_EXTERN_METHOD(openVideo:(NSString *)uri
|
|
19
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
20
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
21
|
+
|
|
22
|
+
RCT_EXTERN_METHOD(deleteTempFile:(NSString *)uri
|
|
23
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
24
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
25
|
+
|
|
26
|
+
RCT_EXTERN_METHOD(cleanupTempFiles:(RCTPromiseResolveBlock)resolve
|
|
27
|
+
reject:(RCTPromiseRejectBlock)reject)
|
|
28
|
+
|
|
29
|
+
@end
|