react-native-video-trim 4.0.0 → 5.0.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.
Files changed (95) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +124 -83
  3. package/VideoTrim.podspec +4 -4
  4. package/android/build.gradle +15 -54
  5. package/android/gradle.properties +2 -0
  6. package/android/src/main/AndroidManifest.xml +1 -1
  7. package/android/src/main/java/com/videotrim/VideoTrimModule.kt +660 -0
  8. package/android/src/main/java/com/videotrim/VideoTrimPackage.kt +33 -0
  9. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/enums/ErrorCode.java +1 -1
  10. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/interfaces/IVideoTrimmerView.java +1 -1
  11. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/interfaces/VideoTrimListener.java +5 -4
  12. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/MediaMetadataUtil.java +1 -1
  13. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/StorageUtil.java +1 -1
  14. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/VideoTrimmerUtil.java +49 -39
  15. package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/widgets/VideoTrimmerView.java +49 -46
  16. package/ios/AssetLoader.h +19 -0
  17. package/ios/AssetLoader.mm +87 -0
  18. package/ios/ErrorCode.h +9 -0
  19. package/ios/ProgressAlertController.h +15 -0
  20. package/ios/ProgressAlertController.mm +78 -0
  21. package/ios/VideoTrim.h +31 -0
  22. package/ios/VideoTrim.mm +663 -0
  23. package/ios/VideoTrimmer.h +67 -0
  24. package/ios/VideoTrimmer.mm +863 -0
  25. package/ios/VideoTrimmerThumb.h +23 -0
  26. package/ios/VideoTrimmerThumb.mm +175 -0
  27. package/ios/VideoTrimmerViewController.h +52 -0
  28. package/ios/VideoTrimmerViewController.mm +533 -0
  29. package/lib/module/NativeVideoTrim.js +5 -0
  30. package/lib/module/NativeVideoTrim.js.map +1 -0
  31. package/lib/module/index.js +59 -34
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/typescript/src/NativeVideoTrim.d.ts +107 -0
  34. package/lib/typescript/src/NativeVideoTrim.d.ts.map +1 -0
  35. package/lib/typescript/src/index.d.ts +21 -10
  36. package/lib/typescript/src/index.d.ts.map +1 -1
  37. package/package.json +15 -18
  38. package/src/NativeVideoTrim.ts +113 -0
  39. package/src/index.tsx +68 -39
  40. package/android/CMakeLists.txt +0 -24
  41. package/android/src/main/cpp/cpp-adapter.cpp +0 -6
  42. package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrim.kt +0 -629
  43. package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrimPackage.kt +0 -22
  44. package/ios/AssetLoader.swift +0 -99
  45. package/ios/ErrorCode.swift +0 -17
  46. package/ios/ProgressAlertController.swift +0 -100
  47. package/ios/VideoTrim.swift +0 -60
  48. package/ios/VideoTrimImpl.swift +0 -860
  49. package/ios/VideoTrimmer.swift +0 -872
  50. package/ios/VideoTrimmerThumb.swift +0 -175
  51. package/ios/VideoTrimmerViewController.swift +0 -557
  52. package/lib/module/VideoTrim.nitro.js +0 -4
  53. package/lib/module/VideoTrim.nitro.js.map +0 -1
  54. package/lib/typescript/src/VideoTrim.nitro.d.ts +0 -240
  55. package/lib/typescript/src/VideoTrim.nitro.d.ts.map +0 -1
  56. package/nitrogen/generated/android/c++/JEditorConfig.hpp +0 -229
  57. package/nitrogen/generated/android/c++/JFileValidationResult.hpp +0 -61
  58. package/nitrogen/generated/android/c++/JFunc_void.hpp +0 -74
  59. package/nitrogen/generated/android/c++/JFunc_void_std__string_std__unordered_map_std__string__std__string_.hpp +0 -89
  60. package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.cpp +0 -131
  61. package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.hpp +0 -67
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/EditorConfig.kt +0 -70
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/FileValidationResult.kt +0 -28
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void.kt +0 -80
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void_std__string_std__unordered_map_std__string__std__string_.kt +0 -80
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/HybridVideoTrimSpec.kt +0 -82
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/videotrimOnLoad.kt +0 -35
  68. package/nitrogen/generated/android/videotrim+autolinking.cmake +0 -78
  69. package/nitrogen/generated/android/videotrim+autolinking.gradle +0 -27
  70. package/nitrogen/generated/android/videotrimOnLoad.cpp +0 -50
  71. package/nitrogen/generated/android/videotrimOnLoad.hpp +0 -25
  72. package/nitrogen/generated/ios/VideoTrim+autolinking.rb +0 -60
  73. package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.cpp +0 -88
  74. package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.hpp +0 -331
  75. package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Umbrella.hpp +0 -53
  76. package/nitrogen/generated/ios/VideoTrimAutolinking.mm +0 -33
  77. package/nitrogen/generated/ios/VideoTrimAutolinking.swift +0 -25
  78. package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.cpp +0 -11
  79. package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.hpp +0 -116
  80. package/nitrogen/generated/ios/swift/EditorConfig.swift +0 -519
  81. package/nitrogen/generated/ios/swift/FileValidationResult.swift +0 -57
  82. package/nitrogen/generated/ios/swift/Func_void.swift +0 -46
  83. package/nitrogen/generated/ios/swift/Func_void_FileValidationResult.swift +0 -46
  84. package/nitrogen/generated/ios/swift/Func_void_bool.swift +0 -46
  85. package/nitrogen/generated/ios/swift/Func_void_double.swift +0 -46
  86. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +0 -46
  87. package/nitrogen/generated/ios/swift/Func_void_std__string_std__unordered_map_std__string__std__string_.swift +0 -54
  88. package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +0 -46
  89. package/nitrogen/generated/ios/swift/HybridVideoTrimSpec.swift +0 -53
  90. package/nitrogen/generated/ios/swift/HybridVideoTrimSpec_cxx.swift +0 -222
  91. package/nitrogen/generated/shared/c++/EditorConfig.hpp +0 -245
  92. package/nitrogen/generated/shared/c++/FileValidationResult.hpp +0 -77
  93. package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.cpp +0 -26
  94. package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.hpp +0 -76
  95. package/src/VideoTrim.nitro.ts +0 -244
@@ -0,0 +1,660 @@
1
+ package com.videotrim
2
+
3
+ import android.R.attr.progressBarStyleHorizontal
4
+ import android.R.attr.selectableItemBackground
5
+ import android.R.color.holo_red_light
6
+ import android.R.style.Theme_Black_NoTitleBar_Fullscreen
7
+ import android.app.Activity
8
+ import android.content.Context
9
+ import android.content.DialogInterface
10
+ import android.content.Intent
11
+ import android.content.pm.PackageManager
12
+ import android.content.res.ColorStateList
13
+ import android.os.Build
14
+ import android.util.Log
15
+ import android.util.TypedValue
16
+ import android.view.Gravity
17
+ import android.view.View
18
+ import android.view.ViewGroup
19
+ import android.widget.Button
20
+ import android.widget.LinearLayout
21
+ import android.widget.ProgressBar
22
+ import android.widget.TextView
23
+ import androidx.appcompat.app.AlertDialog
24
+ import androidx.core.content.ContextCompat
25
+ import androidx.core.content.FileProvider
26
+ import androidx.core.graphics.toColorInt
27
+ import androidx.core.net.toUri
28
+ import androidx.core.view.ViewCompat
29
+ import androidx.core.view.WindowInsetsCompat
30
+ import com.arthenica.ffmpegkit.FFmpegKit
31
+ import com.arthenica.ffmpegkit.ReturnCode
32
+ import com.facebook.react.bridge.*
33
+ import com.facebook.react.module.annotations.ReactModule
34
+ import com.videotrim.enums.ErrorCode
35
+ import com.videotrim.interfaces.VideoTrimListener
36
+ import com.videotrim.utils.MediaMetadataUtil
37
+ import com.videotrim.utils.StorageUtil
38
+ import com.videotrim.widgets.VideoTrimmerView
39
+ import iknow.android.utils.BaseUtils
40
+ import java.io.File
41
+ import java.io.FileInputStream
42
+ import java.io.IOException
43
+ import java.text.SimpleDateFormat
44
+ import java.util.Date
45
+ import java.util.TimeZone
46
+
47
+ @ReactModule(name = VideoTrimModule.NAME)
48
+ class VideoTrimModule(reactContext: ReactApplicationContext) :
49
+ NativeVideoTrimSpec(reactContext), VideoTrimListener, LifecycleEventListener {
50
+
51
+ private var isInit: Boolean = false
52
+ private var trimmerView: VideoTrimmerView? = null
53
+ private var alertDialog: AlertDialog? = null
54
+ private var mProgressDialog: AlertDialog? = null
55
+ private var cancelTrimmingConfirmDialog: AlertDialog? = null
56
+ private var mProgressBar: ProgressBar? = null
57
+ private var outputFile: String? = null
58
+ private var isVideoType = true
59
+ private var editorConfig: ReadableMap? = null
60
+ private var trimOptions: ReadableMap? = null
61
+
62
+ init {
63
+ val mActivityEventListener = object : BaseActivityEventListener() {
64
+ override fun onActivityResult(
65
+ activity: Activity,
66
+ requestCode: Int,
67
+ resultCode: Int,
68
+ intent: Intent?
69
+ ) {
70
+ if (requestCode == REQUEST_CODE_SAVE_FILE && resultCode == Activity.RESULT_OK) {
71
+
72
+ val uri = intent?.data ?: return
73
+ try {
74
+ reactApplicationContext.contentResolver?.openOutputStream(uri)
75
+ ?.use { outputStream ->
76
+ FileInputStream(outputFile).use { fileInputStream ->
77
+ val buffer = ByteArray(1024)
78
+ var length: Int
79
+ while (fileInputStream.read(buffer).also { length = it } > 0) {
80
+ outputStream.write(buffer, 0, length)
81
+ }
82
+ }
83
+ } ?: return
84
+ // File saved successfully
85
+ Log.d(TAG, "File saved successfully to $uri")
86
+
87
+ if (
88
+ editorConfig?.getBoolean("removeAfterSavedToDocuments") == true ||
89
+ trimOptions?.getBoolean("removeAfterFailedToSaveDocuments") == true
90
+ ) {
91
+ StorageUtil.deleteFile(outputFile)
92
+ }
93
+
94
+ } catch (e: Exception) {
95
+ e.printStackTrace()
96
+ // Handle the error
97
+ onError(
98
+ "Failed to save edited video to Documents: ${e.localizedMessage}",
99
+ ErrorCode.FAIL_TO_SAVE_TO_DOCUMENTS
100
+ )
101
+ if (editorConfig?.getBoolean("removeAfterFailedToSaveDocuments") == true || trimOptions?.getBoolean("removeAfterFailedToSaveDocuments") == true) {
102
+ StorageUtil.deleteFile(outputFile)
103
+ }
104
+ } finally {
105
+ hideDialog(true)
106
+ }
107
+ }
108
+ }
109
+ }
110
+ reactApplicationContext.addActivityEventListener(mActivityEventListener)
111
+ }
112
+
113
+ override fun showEditor(
114
+ filePath: String,
115
+ config: ReadableMap,
116
+ ) {
117
+ if (trimmerView != null || alertDialog != null) {
118
+ return
119
+ }
120
+
121
+ this.editorConfig = config
122
+
123
+ this.isVideoType = config.hasKey("type") && config.getString("type") == "video"
124
+
125
+ val activity = reactApplicationContext.currentActivity
126
+ if (!isInit) {
127
+ init()
128
+ isInit = true
129
+ }
130
+
131
+ // here is NOT main thread, we need to create VideoTrimmerView on UI thread, so that later we can update it using same thread
132
+ UiThreadUtil.runOnUiThread {
133
+ trimmerView = VideoTrimmerView(reactApplicationContext, editorConfig, null)
134
+ trimmerView?.setOnTrimVideoListener(this)
135
+ trimmerView?.initByURI(filePath.toUri())
136
+
137
+ val builder = AlertDialog.Builder(
138
+ activity!!, Theme_Black_NoTitleBar_Fullscreen
139
+ )
140
+ builder.setCancelable(false)
141
+ alertDialog = builder.create()
142
+ alertDialog?.setView(trimmerView)
143
+
144
+ // Apply safe area handling after the dialog is shown
145
+ alertDialog?.setOnShowListener {
146
+ applySafeAreaToDialog(alertDialog!!, trimmerView!!)
147
+
148
+ emitOnShow()
149
+ }
150
+
151
+ // this is to ensure to release resource if dialog is dismissed in unexpected way (Eg. open control/notification center by dragging from top of screen)
152
+ alertDialog!!.setOnDismissListener {
153
+ // This is called in same thread as the trimmer view -> UI thread
154
+ if (trimmerView != null) {
155
+ trimmerView!!.onDestroy()
156
+ trimmerView = null
157
+ }
158
+ hideDialog(true)
159
+ emitOnHide()
160
+ }
161
+
162
+ alertDialog?.show()
163
+ }
164
+ }
165
+
166
+ // because trimmerView is rendered within the dialog, we need to apply safe area insets to the dialog
167
+ // else setOnApplyWindowInsetsListener will not fire
168
+ private fun applySafeAreaToDialog(dialog: AlertDialog, trimmerView: VideoTrimmerView) {
169
+ val window = dialog.window
170
+ if (window != null) {
171
+ // Enable edge-to-edge for the dialog window
172
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
173
+ window.setDecorFitsSystemWindows(false)
174
+ }
175
+
176
+ // Get the dialog's decorView and apply insets listener
177
+ val decorView = window.decorView
178
+ ViewCompat.setOnApplyWindowInsetsListener(decorView) { _, windowInsets ->
179
+ val insets = windowInsets.getInsets(
180
+ WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
181
+ )
182
+ Log.d(TAG, "Dialog insets: top=${insets.top}, left=${insets.left}, bottom=${insets.bottom}, right=${insets.right}")
183
+
184
+ // Apply padding to the trimmer view
185
+ trimmerView.setPadding(insets.left, insets.top, insets.right, insets.bottom)
186
+
187
+ WindowInsetsCompat.CONSUMED
188
+ }
189
+ ViewCompat.requestApplyInsets(decorView)
190
+ }
191
+ }
192
+
193
+ private fun init() {
194
+ isInit = true
195
+ // we have to init this before create videoTrimmerView
196
+ BaseUtils.init(reactApplicationContext)
197
+ }
198
+
199
+ override fun onHostResume() {
200
+ Log.d(TAG, "onHostResume: ")
201
+ }
202
+
203
+ override fun onHostPause() {
204
+ Log.d(TAG, "onHostPause: ")
205
+ if (trimmerView != null) {
206
+ trimmerView!!.onMediaPause()
207
+ }
208
+ }
209
+
210
+ override fun onHostDestroy() {
211
+ hideDialog(true)
212
+ }
213
+
214
+ override fun invalidate() {
215
+ super.invalidate()
216
+ hideDialog(true)
217
+ }
218
+
219
+ override fun onLoad(duration: Int) {
220
+ val map = Arguments.createMap()
221
+ map.putInt("duration", duration)
222
+ emitOnLoad(map)
223
+ }
224
+
225
+ override fun onTrimmingProgress(percentage: Int) {
226
+ // prevent onTrimmingProgress is called after onFinishTrim (some rare cases)
227
+ if (mProgressBar == null) {
228
+ return
229
+ }
230
+
231
+ mProgressBar!!.setProgress(percentage, true)
232
+ }
233
+
234
+
235
+ override fun onFinishTrim(out: String, startTime: Long, endTime: Long, duration: Int) {
236
+ // save output file to use in other places
237
+ outputFile = out
238
+
239
+ val map = Arguments.createMap()
240
+ map.putString("outputPath", outputFile)
241
+ map.putInt("duration", duration)
242
+ map.putDouble("startTime", startTime.toDouble())
243
+ map.putDouble("endTime", endTime.toDouble())
244
+ emitOnFinishTrimming(map)
245
+
246
+ if (editorConfig?.getBoolean("saveToPhoto") == true && isVideoType) {
247
+ try {
248
+ StorageUtil.saveVideoToGallery(reactApplicationContext, outputFile)
249
+ Log.d(TAG, "Edited video saved to Photo Library successfully.")
250
+ if (editorConfig?.getBoolean("removeAfterSavedToPhoto") == true) {
251
+ StorageUtil.deleteFile(outputFile)
252
+ }
253
+ } catch (e: IOException) {
254
+ e.printStackTrace()
255
+ onError(
256
+ "Failed to save edited video to Photo Library: " + e.localizedMessage,
257
+ ErrorCode.FAIL_TO_SAVE_TO_PHOTO
258
+ )
259
+ if (editorConfig?.getBoolean("removeAfterFailedToSavePhoto") == true) {
260
+ StorageUtil.deleteFile(outputFile)
261
+ }
262
+ } finally {
263
+ hideDialog(editorConfig?.getBoolean("closeWhenFinish") ?: true)
264
+ }
265
+ } else if (editorConfig?.getBoolean("openDocumentsOnFinish") == true) {
266
+ saveFileToExternalStorage(File(outputFile!!))
267
+ } else if (editorConfig?.getBoolean("openShareSheetOnFinish") == true) {
268
+ hideDialog(editorConfig?.getBoolean("closeWhenFinish") ?: true)
269
+ shareFile(reactApplicationContext, File(outputFile!!))
270
+ } else {
271
+ hideDialog(editorConfig?.getBoolean("closeWhenFinish") ?: true)
272
+ }
273
+ }
274
+
275
+ override fun onCancelTrim() {
276
+ emitOnCancelTrimming()
277
+ }
278
+
279
+ override fun onError(errorMessage: String?, errorCode: ErrorCode) {
280
+ val map = Arguments.createMap()
281
+ map.putString("message", errorMessage)
282
+ map.putString("errorCode", errorCode.name)
283
+ emitOnError(map)
284
+ }
285
+
286
+ override fun onCancel() {
287
+ if (!editorConfig?.getBoolean("enableCancelDialog")!!) {
288
+ emitOnCancel()
289
+ hideDialog(true)
290
+ return
291
+ }
292
+
293
+ val builder = AlertDialog.Builder(reactApplicationContext.currentActivity!!)
294
+ builder.setMessage(editorConfig?.getString("cancelDialogMessage"))
295
+ builder.setTitle(editorConfig?.getString("cancelDialogTitle"))
296
+ builder.setCancelable(false)
297
+ builder.setPositiveButton(editorConfig?.getString("cancelDialogConfirmText")) { dialog: DialogInterface, _: Int ->
298
+ dialog.cancel()
299
+ emitOnCancel()
300
+ hideDialog(true)
301
+ }
302
+ builder.setNegativeButton(
303
+ editorConfig?.getString("cancelDialogCancelText") ?: "Cancel"
304
+ ) { dialog: DialogInterface, _: Int ->
305
+ dialog.cancel()
306
+ }
307
+ val alertDialog = builder.create()
308
+ alertDialog.show()
309
+ }
310
+
311
+ override fun onSave() {
312
+ if (!editorConfig?.getBoolean("enableSaveDialog")!!) {
313
+ startTrim()
314
+ return
315
+ }
316
+
317
+ val builder = AlertDialog.Builder(reactApplicationContext.currentActivity!!)
318
+ builder.setMessage(editorConfig?.getString("saveDialogMessage"))
319
+ builder.setTitle(editorConfig?.getString("saveDialogTitle"))
320
+ builder.setCancelable(false)
321
+ builder.setPositiveButton(editorConfig?.getString("saveDialogConfirmText")) { dialog: DialogInterface, _: Int ->
322
+ dialog.cancel()
323
+ startTrim()
324
+ }
325
+ builder.setNegativeButton(
326
+ editorConfig?.getString("saveDialogCancelText") ?: "Cancel"
327
+ ) { dialog: DialogInterface, _: Int ->
328
+ dialog.cancel()
329
+ }
330
+ val alertDialog = builder.create()
331
+ alertDialog.show()
332
+ }
333
+
334
+ override fun onLog(log: ReadableMap) {
335
+ emitOnLog( log)
336
+ }
337
+
338
+ override fun onStatistics(statistics: ReadableMap) {
339
+ emitOnStatistics(statistics)
340
+ }
341
+
342
+ private fun startTrim() {
343
+ val activity = reactApplicationContext.currentActivity
344
+ // Create the parent layout for the dialog
345
+ val layout = LinearLayout(activity)
346
+ layout.layoutParams = ViewGroup.LayoutParams(
347
+ ViewGroup.LayoutParams.WRAP_CONTENT,
348
+ ViewGroup.LayoutParams.WRAP_CONTENT
349
+ )
350
+ layout.orientation = LinearLayout.VERTICAL
351
+ layout.gravity = Gravity.CENTER_HORIZONTAL
352
+ layout.setPadding(16, 32, 16, 32)
353
+
354
+ // Create and add the TextView
355
+ val textView = TextView(activity)
356
+ textView.layoutParams = ViewGroup.LayoutParams(
357
+ ViewGroup.LayoutParams.WRAP_CONTENT,
358
+ ViewGroup.LayoutParams.WRAP_CONTENT
359
+ )
360
+ textView.text = editorConfig?.getString("trimmingText")
361
+ ?: "Trimming in progress..."
362
+ textView.gravity = Gravity.CENTER
363
+ textView.textSize = 18f
364
+ layout.addView(textView)
365
+
366
+ // Create and add the ProgressBar
367
+ mProgressBar = ProgressBar(activity, null, progressBarStyleHorizontal)
368
+ mProgressBar!!.layoutParams = ViewGroup.LayoutParams(
369
+ ViewGroup.LayoutParams.MATCH_PARENT,
370
+ ViewGroup.LayoutParams.WRAP_CONTENT
371
+ )
372
+ mProgressBar!!.progressTintList = ColorStateList.valueOf("#2196F3".toColorInt())
373
+ layout.addView(mProgressBar)
374
+
375
+ // Create button
376
+ if (editorConfig?.getBoolean("enableCancelTrimming") == true) {
377
+ val button = Button(activity)
378
+ button.layoutParams = ViewGroup.LayoutParams(
379
+ ViewGroup.LayoutParams.WRAP_CONTENT,
380
+ ViewGroup.LayoutParams.WRAP_CONTENT
381
+ )
382
+ // Set the text and style it like a text button
383
+ button.text = editorConfig?.getString("cancelTrimmingText")
384
+ ?: "Cancel Trimming"
385
+ button.setTextColor(
386
+ ContextCompat.getColor(
387
+ activity!!,
388
+ holo_red_light
389
+ )
390
+ ) // or use your custom color
391
+
392
+ // Apply ripple effect while keeping the button background transparent
393
+ val outValue = TypedValue()
394
+ activity.theme.resolveAttribute(selectableItemBackground, outValue, true)
395
+ button.setBackgroundResource(outValue.resourceId)
396
+ button.setOnClickListener { _: View? ->
397
+ if (editorConfig?.getBoolean("enableCancelTrimmingDialog") == true) {
398
+ val builder = AlertDialog.Builder(
399
+ activity
400
+ )
401
+ builder.setMessage(editorConfig?.getString("cancelTrimmingDialogMessage"))
402
+ builder.setTitle(editorConfig?.getString("cancelTrimmingDialogTitle"))
403
+ builder.setCancelable(false)
404
+ builder.setPositiveButton(editorConfig?.getString("cancelTrimmingDialogConfirmText")) { _: DialogInterface?, _: Int ->
405
+ if (trimmerView != null) {
406
+ trimmerView!!.onCancelTrimClicked()
407
+ }
408
+ if (mProgressDialog != null && mProgressDialog!!.isShowing) {
409
+ mProgressDialog!!.dismiss()
410
+ }
411
+ }
412
+ builder.setNegativeButton(
413
+ editorConfig?.getString("cancelTrimmingDialogCancelText") ?: "Close"
414
+ ) { dialog: DialogInterface, _: Int ->
415
+ dialog.cancel()
416
+ }
417
+ cancelTrimmingConfirmDialog = builder.create()
418
+ cancelTrimmingConfirmDialog!!.show()
419
+ } else {
420
+ if (trimmerView != null) {
421
+ trimmerView!!.onCancelTrimClicked()
422
+ }
423
+
424
+ if (mProgressDialog != null && mProgressDialog!!.isShowing) {
425
+ mProgressDialog!!.dismiss()
426
+ }
427
+ }
428
+ }
429
+ layout.addView(button)
430
+ }
431
+
432
+ // Create the AlertDialog
433
+ val builder = AlertDialog.Builder(
434
+ activity!!
435
+ )
436
+ builder.setCancelable(false)
437
+ builder.setView(layout)
438
+
439
+ // Show the dialog
440
+ mProgressDialog = builder.create()
441
+
442
+ mProgressDialog!!.setOnShowListener {
443
+ emitOnStartTrimming()
444
+ if (trimmerView != null) {
445
+ trimmerView!!.onSaveClicked()
446
+ }
447
+ }
448
+
449
+ mProgressDialog!!.show()
450
+ }
451
+
452
+ private fun hideDialog(shouldCloseEditor: Boolean) {
453
+ // handle the case when the cancel dialog is still showing but the trimming is finished
454
+ if (cancelTrimmingConfirmDialog != null) {
455
+ if (cancelTrimmingConfirmDialog!!.isShowing) {
456
+ cancelTrimmingConfirmDialog!!.dismiss()
457
+ }
458
+ cancelTrimmingConfirmDialog = null
459
+ }
460
+
461
+ if (mProgressDialog != null) {
462
+ if (mProgressDialog!!.isShowing) mProgressDialog!!.dismiss()
463
+ mProgressBar = null
464
+ mProgressDialog = null
465
+ }
466
+
467
+ if (shouldCloseEditor) {
468
+ if (alertDialog != null) {
469
+ if (alertDialog!!.isShowing) {
470
+ alertDialog!!.dismiss()
471
+ }
472
+ alertDialog = null
473
+ }
474
+ }
475
+ }
476
+
477
+ // private fun sendEvent(
478
+ // eventName: String,
479
+ // params: Map<String, String>
480
+ // ) {
481
+ // onEvent?.let { it(eventName, params) }
482
+ //
483
+ // if (eventName == "onHide" && onComplete != null) {
484
+ // onComplete?.let { it() }
485
+ // onComplete = null // Clear the callback after invoking it
486
+ // }
487
+ // }
488
+
489
+ override fun listFiles(promise: Promise) {
490
+ promise.resolve(StorageUtil.listFiles(reactApplicationContext))
491
+ }
492
+
493
+ override fun cleanFiles(promise: Promise) {
494
+ val files = StorageUtil.listFiles(reactApplicationContext)
495
+ var successCount = 0
496
+ for (file in files) {
497
+ val state = StorageUtil.deleteFile(file)
498
+ if (state) {
499
+ successCount++
500
+ }
501
+ }
502
+
503
+ promise.resolve(successCount.toDouble())
504
+ }
505
+
506
+ override fun deleteFile(filePath: String?, promise: Promise) {
507
+ promise.resolve(StorageUtil.deleteFile(filePath))
508
+ }
509
+
510
+ override fun closeEditor() {
511
+ hideDialog(true)
512
+ emitOnHide()
513
+ }
514
+
515
+ override fun isValidFile(url: String, promise: Promise) {
516
+ MediaMetadataUtil.checkFileValidity(url) { isValid: Boolean, fileType: String, duration: Long ->
517
+ if (isValid) {
518
+ Log.d(TAG, "Valid $fileType file with duration: $duration milliseconds")
519
+ } else {
520
+ Log.d(TAG, "Invalid file")
521
+ }
522
+ // Create a FileValidationResult object
523
+
524
+ val result = Arguments.createMap()
525
+ result.putBoolean("isValid", isValid)
526
+ result.putString("fileType", fileType)
527
+ result.putDouble("duration", duration.toDouble())
528
+
529
+ promise.resolve(result)
530
+ }
531
+ }
532
+
533
+ override fun trim(url: String, options: ReadableMap?, promise: Promise) {
534
+ trimOptions = options
535
+
536
+ val currentDate = Date()
537
+ val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
538
+
539
+ dateFormat.timeZone = TimeZone.getTimeZone("UTC")
540
+ val formattedDateTime = dateFormat.format(currentDate)
541
+
542
+ var cmds = arrayOf(
543
+ "-ss",
544
+ "${options?.getDouble("startTime") ?: 0 }ms",
545
+ "-to",
546
+ "${options?.getDouble("endTime") ?: 1000}ms",
547
+ )
548
+
549
+ if (options?.getBoolean("enableRotation") == true) {
550
+ cmds += arrayOf("-display_rotation", "${options.getDouble("rotationAngle")}")
551
+ }
552
+
553
+ outputFile = StorageUtil.getOutputPath(reactApplicationContext, options?.getString("outputExt") ?: "mp4")
554
+
555
+ cmds += arrayOf(
556
+ "-i",
557
+ url,
558
+ "-c",
559
+ "copy",
560
+ "-metadata",
561
+ "creation_time=$formattedDateTime",
562
+ outputFile!!
563
+ )
564
+
565
+ Log.d(TAG, "Command: ${cmds.joinToString(",")}")
566
+
567
+ FFmpegKit.executeWithArgumentsAsync(cmds, { session ->
568
+ val state = session.state
569
+ val returnCode = session.returnCode
570
+ when {
571
+ ReturnCode.isSuccess(returnCode) -> {
572
+ // SUCCESS
573
+ if (options?.getBoolean("saveToPhoto") == true && options.getString("type") == "video") {
574
+ try {
575
+ StorageUtil.saveVideoToGallery(reactApplicationContext, outputFile)
576
+ Log.d(TAG, "Edited video saved to Photo Library successfully.")
577
+ if (options.getBoolean("removeAfterSavedToPhoto")) {
578
+ StorageUtil.deleteFile(outputFile)
579
+ }
580
+
581
+ promise.resolve(outputFile)
582
+ } catch (e: IOException) {
583
+ e.printStackTrace()
584
+
585
+ if (options.getBoolean("removeAfterFailedToSavePhoto")) {
586
+ StorageUtil.deleteFile(outputFile)
587
+ }
588
+
589
+ promise.reject(
590
+ Exception("Failed to save edited video to Photo Library: " + e.localizedMessage)
591
+ )
592
+ }
593
+ } else {
594
+ if (options?.getBoolean("openDocumentsOnFinish") == true) {
595
+ saveFileToExternalStorage(File(outputFile!!))
596
+ } else if (options?.getBoolean("openShareSheetOnFinish") == true) {
597
+ shareFile(reactApplicationContext, File(outputFile!!))
598
+ }
599
+
600
+ promise.resolve(outputFile)
601
+ }
602
+ }
603
+ ReturnCode.isCancel(returnCode) -> {
604
+ // CANCEL
605
+ println("FFmpeg command was cancelled")
606
+ promise.reject(
607
+ Exception("FFmpeg command was cancelled")
608
+ )
609
+ }
610
+ else -> {
611
+ // FAILURE
612
+ val errorMessage = String.format("Command failed with state %s and rc %s.%s", state, returnCode, session.getFailStackTrace());
613
+ println(errorMessage)
614
+ promise.reject(
615
+ Exception(errorMessage)
616
+ )
617
+ }
618
+ }
619
+ }, { log ->
620
+ Log.d(TAG, "FFmpeg process started with log ${log.message}")
621
+ }, { statistics ->
622
+ // Handle statistics if needed
623
+ })
624
+ }
625
+
626
+ private fun saveFileToExternalStorage(file: File) {
627
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
628
+ intent.addCategory(Intent.CATEGORY_OPENABLE)
629
+ intent.setType("*/*") // Change MIME type as needed
630
+ intent.putExtra(Intent.EXTRA_TITLE, file.name)
631
+ reactApplicationContext.currentActivity?.startActivityForResult(intent, REQUEST_CODE_SAVE_FILE)
632
+ }
633
+
634
+ private fun shareFile(context: Context, file: File) {
635
+ val fileUri = FileProvider.getUriForFile(context, context.packageName + ".provider", file)
636
+
637
+ val shareIntent = Intent(Intent.ACTION_SEND)
638
+ shareIntent.setType("*/*")
639
+ shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri)
640
+ shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
641
+
642
+ // Grant permissions to all applications that can handle the intent
643
+ for (resolveInfo in context.packageManager.queryIntentActivities(
644
+ shareIntent,
645
+ PackageManager.MATCH_DEFAULT_ONLY
646
+ )) {
647
+ val packageName = resolveInfo.activityInfo.packageName
648
+ context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
649
+ }
650
+
651
+ // directly use context.startActivity(shareIntent) will cause crash
652
+ reactApplicationContext.currentActivity?.startActivity(Intent.createChooser(shareIntent, "Share file"))
653
+ }
654
+
655
+ companion object {
656
+ const val NAME = "VideoTrim"
657
+ const val TAG = "VideoTrimModule"
658
+ const val REQUEST_CODE_SAVE_FILE = 1
659
+ }
660
+ }
@@ -0,0 +1,33 @@
1
+ package com.videotrim
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class VideoTrimPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == VideoTrimModule.NAME) {
13
+ VideoTrimModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
20
+ return ReactModuleInfoProvider {
21
+ val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
22
+ moduleInfos[VideoTrimModule.NAME] = ReactModuleInfo(
23
+ VideoTrimModule.NAME,
24
+ VideoTrimModule.NAME,
25
+ false, // canOverrideExistingModule
26
+ false, // needsEagerInit
27
+ false, // isCxxModule
28
+ true // isTurboModule
29
+ )
30
+ moduleInfos
31
+ }
32
+ }
33
+ }
@@ -1,4 +1,4 @@
1
- package com.margelo.nitro.videotrim.enums;
1
+ package com.videotrim.enums;
2
2
 
3
3
  public enum ErrorCode {
4
4
  TRIMMING_FAILED,
@@ -1,4 +1,4 @@
1
- package com.margelo.nitro.videotrim.interfaces;
1
+ package com.videotrim.interfaces;
2
2
 
3
3
  public interface IVideoTrimmerView {
4
4
  void onDestroy();