shakebug-react-native 2.4.9 → 2.4.11

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 (51) hide show
  1. package/README.md +0 -12
  2. package/VideoPlayer/PlayVideo.tsx +4 -46
  3. package/VideoPlayer/index.js +6 -43
  4. package/android/src/main/java/com/shakebug/BaseUtils.java +7 -19
  5. package/android/src/main/java/com/shakebug/DeviceUtil.java +34 -21
  6. package/android/src/main/java/com/shakebug/ShakebugPackage.java +3 -3
  7. package/android/src/main/java/com/shakebug/UnitConverter.java +8 -8
  8. package/android/src/main/java/com/shakebug/VideoPlayerModule.java +81 -0
  9. package/android/src/main/java/com/shakebug/VideoTrimModule.java +387 -0
  10. package/android/src/main/java/com/shakebug/VideoTrimUtil.java +133 -0
  11. package/android/src/main/java/com/shakebug/adapters/VideoTrimmerAdapter.java +53 -0
  12. package/android/src/main/java/com/shakebug/callback/SingleCallback.java +0 -6
  13. package/android/src/main/java/com/shakebug/thread/BackgroundExecutor.java +1 -93
  14. package/android/src/main/java/com/shakebug/thread/UiThreadExecutor.java +0 -37
  15. package/android/src/main/java/com/shakebug/utils/StorageUtil.java +135 -0
  16. package/android/src/main/java/com/shakebug/utils/VideoTrimmerUtil.java +70 -0
  17. package/android/src/main/java/com/shakebug/widgets/RangeSeekBarView.java +534 -0
  18. package/android/src/main/java/com/shakebug/widgets/SpacesItemDecoration2.java +1 -2
  19. package/android/src/main/java/com/shakebug/widgets/VideoPlayerView.java +317 -0
  20. package/android/src/main/java/com/shakebug/widgets/VideoTrimmerView.java +520 -0
  21. package/android/src/main/res/drawable/ic_volume_off.xml +10 -0
  22. package/android/src/main/res/drawable/ic_volume_on.xml +10 -0
  23. package/android/src/main/res/layout/{video_player_view.xml → shakebug_video_player_view.xml} +29 -9
  24. package/ios/TrimVideo/ShakebugSDKVideoTrim.swift +389 -0
  25. package/ios/TrimVideo/VideoTrim-Bridging-Header.h +10 -0
  26. package/ios/TrimVideo/VideoTrim.m +23 -0
  27. package/ios/TrimVideo/VideoTrimmer.swift +810 -0
  28. package/ios/TrimVideo/VideoTrimmerThumb.swift +119 -0
  29. package/ios/TrimVideo/VideoTrimmerViewController.swift +322 -0
  30. package/ios/VideoPlayer/VideoPlayerModule.m +23 -7
  31. package/package.json +1 -1
  32. package/src/APIs/AddIssue.js +7 -5
  33. package/src/APIs/AddReview.js +5 -2
  34. package/src/APIs/CustomUserId.js +5 -2
  35. package/src/APIs/EndSession.js +3 -2
  36. package/src/APIs/Event.js +5 -2
  37. package/src/APIs/FetchRemoteCodeAPI.js +5 -2
  38. package/src/APIs/MatchKey.js +3 -2
  39. package/src/APIs/StartSession.js +44 -23
  40. package/src/Componanats/LoadingModal.js +59 -0
  41. package/src/Componanats/ShakeFunction.js +32 -14
  42. package/src/Screens/AddDetailsScreen.js +28 -31
  43. package/src/Screens/FirstScreen.js +15 -18
  44. package/src/Screens/Shakebug.js +11 -6
  45. package/src/Screens/TrimVideoScreen.js +37 -22
  46. package/src/index.tsx +7 -6
  47. package/trimVideo/src/index.tsx +12 -48
  48. package/Volumes:.fileloc +0 -8
  49. package/shakebug-react-native-2.4.8.tgz +0 -0
  50. /package/android/src/main/res/layout/{video_thumb_item_layout.xml → shakebug_video_thumb_item_layout.xml} +0 -0
  51. /package/android/src/main/res/layout/{video_trimmer_view.xml → shakebug_video_trimmer_view.xml} +0 -0
@@ -0,0 +1,389 @@
1
+ //
2
+ // VideoTrim.swift
3
+ // Shakebug
4
+ //
5
+
6
+ import React
7
+ import Photos
8
+ import AVFoundation
9
+
10
+ @objc(ShakebugSDKVideoTrim)
11
+ class ShakebugSDKVideoTrim: RCTEventEmitter {
12
+ private let FILE_PREFIX = "trimmedVideo"
13
+ private var hasListeners = false
14
+ private var isShowing = false
15
+
16
+ private var saveToPhoto = true
17
+ private var removeAfterSavedToPhoto = false
18
+ private var trimmingText = "Trimming video..."
19
+
20
+ @objc
21
+ static override func requiresMainQueueSetup() -> Bool {
22
+ return true
23
+ }
24
+
25
+ override func supportedEvents() -> [String]! {
26
+ return ["ShakebugSDKVideoTrim", "VideoTrim"]
27
+ }
28
+
29
+ override func startObserving() {
30
+ hasListeners = true
31
+ }
32
+
33
+ override func stopObserving() {
34
+ hasListeners = false
35
+ }
36
+
37
+ @objc(isValidVideo:withResolver:withRejecter:)
38
+ func isValidVideo(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
39
+ if let destPath = copyFileToDocumentDir(uri: uri) {
40
+ resolve(UIVideoEditorController.canEditVideo(atPath: destPath.path))
41
+ let _ = deleteFile(url: destPath)
42
+ } else {
43
+ resolve(false)
44
+ }
45
+ }
46
+
47
+ @objc(showEditor:withConfig:)
48
+ func showEditor(uri: String, config: NSDictionary){
49
+ if isShowing {
50
+ return
51
+ }
52
+
53
+ saveToPhoto = config["saveToPhoto"] as? Bool ?? true
54
+ removeAfterSavedToPhoto = config["removeAfterSavedToPhoto"] as? Bool ?? false
55
+
56
+ let enableCancelDialog = config["enableCancelDialog"] as? Bool ?? true
57
+ let cancelDialogTitle = config["cancelDialogTitle"] as? String ?? "Warning!"
58
+ let cancelDialogMessage = config["cancelDialogMessage"] as? String ?? "Are you sure want to cancel?"
59
+ let cancelDialogCancelText = config["cancelDialogCancelText"] as? String ?? "Close"
60
+ let cancelDialogConfirmText = config["cancelDialogConfirmText"] as? String ?? "Proceed"
61
+
62
+ let enableSaveDialog = config["enableSaveDialog"] as? Bool ?? true
63
+ let saveDialogTitle = config["saveDialogTitle"] as? String ?? "Confirmation!"
64
+ let saveDialogMessage = config["saveDialogMessage"] as? String ?? "Are you sure want to save?"
65
+ let saveDialogCancelText = config["saveDialogCancelText"] as? String ?? "Close"
66
+ let saveDialogConfirmText = config["saveDialogConfirmText"] as? String ?? "Proceed"
67
+ trimmingText = config["trimmingText"] as? String ?? "Trimming video..."
68
+ let fullScreenModalIOS = config["fullScreenModalIOS"] as? Bool ?? false
69
+
70
+ DispatchQueue.main.async {
71
+ let presentTrimmer = { (destPath: URL) in
72
+ if UIVideoEditorController.canEditVideo(atPath: destPath.path) {
73
+ if #available(iOS 13.0, *) {
74
+ let vc = VideoTrimmerViewController()
75
+ vc.asset = AVURLAsset(url: destPath, options: [AVURLAssetPreferPreciseDurationAndTimingKey: true])
76
+
77
+ if let maxDuration = config["maxDuration"] as? Int {
78
+ vc.maximumDuration = maxDuration
79
+ }
80
+
81
+ if let minDuration = config["minDuration"] as? Int {
82
+ vc.minimumDuration = minDuration
83
+ }
84
+
85
+ if let cancelBtnText = config["cancelButtonText"] as? String, !cancelBtnText.isEmpty {
86
+ vc.cancelBtnText = cancelBtnText
87
+ }
88
+
89
+ if let saveButtonText = config["saveButtonText"] as? String, !saveButtonText.isEmpty {
90
+ vc.saveButtonText = saveButtonText
91
+ }
92
+
93
+ vc.cancelBtnClicked = {
94
+ if !enableCancelDialog {
95
+ let _ = self.deleteFile(url: destPath)
96
+ self.emitEventToJS("onCancel", eventData: nil)
97
+ vc.dismiss(animated: true, completion: {
98
+ self.emitEventToJS("onHide", eventData: nil)
99
+ self.isShowing = false
100
+ })
101
+ return
102
+ }
103
+
104
+ let cancelDialog = UIAlertController(title: cancelDialogTitle, message: cancelDialogMessage, preferredStyle: .alert)
105
+
106
+ let ok = UIAlertAction(title: cancelDialogConfirmText, style: .default, handler: { (action) -> Void in
107
+ let _ = self.deleteFile(url: destPath)
108
+ self.emitEventToJS("onCancel", eventData: nil)
109
+ vc.dismiss(animated: true, completion: {
110
+ self.emitEventToJS("onHide", eventData: nil)
111
+ self.isShowing = false
112
+ })
113
+ })
114
+
115
+ let cancel = UIAlertAction(title: cancelDialogCancelText, style: .cancel)
116
+
117
+ cancelDialog.addAction(ok)
118
+ cancelDialog.addAction(cancel)
119
+
120
+ vc.present(cancelDialog, animated: true, completion: nil)
121
+ }
122
+
123
+ vc.saveBtnClicked = { (selectedRange) in
124
+ if !enableSaveDialog {
125
+ self.trim(viewController: vc, inputFile: destPath, videoDuration: vc.asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
126
+ return
127
+ }
128
+
129
+ let saveDialog = UIAlertController(title: saveDialogTitle, message: saveDialogMessage, preferredStyle: .alert)
130
+
131
+ let ok = UIAlertAction(title: saveDialogConfirmText, style: .default, handler: { (action) -> Void in
132
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
133
+ self.trim(viewController: vc, inputFile: destPath, videoDuration: vc.asset.duration.seconds, startTime: selectedRange.start.seconds, endTime: selectedRange.end.seconds)
134
+ }
135
+ })
136
+
137
+ let cancel = UIAlertAction(title: saveDialogCancelText, style: .cancel)
138
+
139
+ saveDialog.addAction(ok)
140
+ saveDialog.addAction(cancel)
141
+
142
+ vc.present(saveDialog, animated: true, completion: nil)
143
+ }
144
+
145
+ vc.isModalInPresentation = true
146
+ vc.modalPresentationStyle = fullScreenModalIOS ? .fullScreen : .popover
147
+ if let root = RCTPresentedViewController() {
148
+ root.present(vc, animated: true, completion: {
149
+ self.isShowing = true
150
+ self.emitEventToJS("onShow", eventData: nil)
151
+ })
152
+ }
153
+ }
154
+ } else {
155
+ let eventPayload: [String: Any] = ["message": "File is not a valid video"]
156
+ self.emitEventToJS("onError", eventData: eventPayload)
157
+ }
158
+ }
159
+
160
+ let dialogMessage = UIAlertController(title: "Loading video...", message: nil, preferredStyle: .alert)
161
+ let customVC = UIViewController()
162
+ customVC.view.backgroundColor = .clear
163
+ customVC.preferredContentSize = CGSize(width: 250, height: 50)
164
+ let indicator = UIActivityIndicatorView(style: .medium)
165
+ indicator.startAnimating()
166
+ indicator.translatesAutoresizingMaskIntoConstraints = false
167
+ customVC.view.addSubview(indicator)
168
+ NSLayoutConstraint.activate([
169
+ indicator.centerXAnchor.constraint(equalTo: customVC.view.centerXAnchor),
170
+ indicator.centerYAnchor.constraint(equalTo: customVC.view.centerYAnchor)
171
+ ])
172
+ dialogMessage.setValue(customVC, forKey: "contentViewController")
173
+ if let root = RCTPresentedViewController() {
174
+ root.present(dialogMessage, animated: true) {
175
+ DispatchQueue.global(qos: .userInitiated).async {
176
+ if let destPath = self.copyFileToDocumentDir(uri: uri) {
177
+ DispatchQueue.main.async {
178
+ dialogMessage.dismiss(animated: true) {
179
+ presentTrimmer(destPath)
180
+ }
181
+ }
182
+ } else {
183
+ DispatchQueue.main.async {
184
+ dialogMessage.dismiss(animated: true) {
185
+ let eventPayload: [String: Any] = ["message": "File is invalid"]
186
+ self.emitEventToJS("onError", eventData: eventPayload)
187
+ }
188
+ }
189
+ }
190
+ }
191
+ }
192
+ } else {
193
+ DispatchQueue.global(qos: .userInitiated).async {
194
+ if let destPath = self.copyFileToDocumentDir(uri: uri) {
195
+ DispatchQueue.main.async {
196
+ presentTrimmer(destPath)
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ private func copyFileToDocumentDir(uri: String) -> URL? {
205
+ var cleanUri = uri.replacingOccurrences(of: "file://", with: "")
206
+ if let decoded = cleanUri.removingPercentEncoding {
207
+ cleanUri = decoded
208
+ }
209
+ let fileURL = cleanUri.hasPrefix("/") ? URL(fileURLWithPath: cleanUri) : URL(string: uri)
210
+ guard let validURL = fileURL else { return nil }
211
+
212
+ let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
213
+ let fileExtension = validURL.pathExtension.isEmpty ? "mp4" : validURL.pathExtension
214
+ let timestamp = Int(Date().timeIntervalSince1970)
215
+ let destinationURL = documentsDirectory.appendingPathComponent("\(FILE_PREFIX)_original_\(timestamp).\(fileExtension)")
216
+
217
+ do {
218
+ if FileManager.default.fileExists(atPath: destinationURL.path) {
219
+ try FileManager.default.removeItem(at: destinationURL)
220
+ }
221
+ try FileManager.default.copyItem(at: validURL, to: destinationURL)
222
+ return destinationURL
223
+ } catch {
224
+ print("Error while copying file to document directory \(error)")
225
+ return validURL
226
+ }
227
+ }
228
+
229
+ private func emitEventToJS(_ eventName: String, eventData: [String: Any]?) {
230
+ if hasListeners {
231
+ var modifiedEventData = eventData ?? [:]
232
+ modifiedEventData["name"] = eventName
233
+ sendEvent(withName: "VideoTrim", body: modifiedEventData)
234
+ }
235
+ }
236
+
237
+ @objc(listFiles:withRejecter:)
238
+ func listFiles(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
239
+ let files = listFiles()
240
+ resolve(files.map{ $0.absoluteString })
241
+ }
242
+
243
+ @objc(cleanFiles:withRejecter:)
244
+ func cleanFiles(resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
245
+ let files = listFiles()
246
+ var successCount = 0
247
+ for file in files {
248
+ let state = deleteFile(url: file)
249
+ if state == 0 {
250
+ successCount += 1
251
+ }
252
+ }
253
+ resolve(successCount)
254
+ }
255
+
256
+ @objc(deleteFile:withResolver:withRejecter:)
257
+ func deleteFile(uri: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) -> Void {
258
+ var cleanUri = uri.replacingOccurrences(of: "file://", with: "")
259
+ if let decoded = cleanUri.removingPercentEncoding {
260
+ cleanUri = decoded
261
+ }
262
+ let fileURL = cleanUri.hasPrefix("/") ? URL(fileURLWithPath: cleanUri) : URL(string: uri)
263
+ if let url = fileURL {
264
+ let state = deleteFile(url: url)
265
+ resolve(state == 0)
266
+ } else {
267
+ resolve(false)
268
+ }
269
+ }
270
+
271
+ private func listFiles() -> [URL] {
272
+ var files: [URL] = []
273
+ let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
274
+ do {
275
+ let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil)
276
+ for fileURL in directoryContents {
277
+ if fileURL.lastPathComponent.starts(with: FILE_PREFIX) {
278
+ files.append(fileURL)
279
+ }
280
+ }
281
+ } catch {
282
+ print("[listFiles] Error when retrieving files: \(error)")
283
+ }
284
+ return files
285
+ }
286
+
287
+ private func deleteFile(url: URL) -> Int {
288
+ do {
289
+ if FileManager.default.fileExists(atPath: url.path) {
290
+ try FileManager.default.removeItem(at: url)
291
+ return 0
292
+ }
293
+ return 1
294
+ } catch {
295
+ print("[deleteFile] Error deleting files: \(error)")
296
+ return 2
297
+ }
298
+ }
299
+
300
+ @available(iOS 13.0, *)
301
+ private func trim(viewController: VideoTrimmerViewController, inputFile: URL, videoDuration: Double, startTime: Double, endTime: Double) {
302
+ let timestamp = Int(Date().timeIntervalSince1970)
303
+ let outputName = "\(FILE_PREFIX)_\(timestamp).mp4"
304
+ let outputURL = inputFile.deletingLastPathComponent().appendingPathComponent(outputName)
305
+
306
+ self.emitEventToJS("onStartTrimming", eventData: nil)
307
+
308
+ let dialogMessage = UIAlertController(title: trimmingText, message: nil, preferredStyle: .alert)
309
+
310
+ let customVC = UIViewController()
311
+ customVC.view.backgroundColor = .clear
312
+ customVC.preferredContentSize = CGSize(width: 250, height: 50)
313
+
314
+ let progressView = UIProgressView(frame: .zero)
315
+ progressView.tintColor = .systemBlue
316
+ customVC.view.addSubview(progressView)
317
+ progressView.translatesAutoresizingMaskIntoConstraints = false
318
+ NSLayoutConstraint.activate([
319
+ progressView.leadingAnchor.constraint(equalTo: customVC.view.leadingAnchor, constant: 16),
320
+ progressView.trailingAnchor.constraint(equalTo: customVC.view.trailingAnchor, constant: -16),
321
+ progressView.centerYAnchor.constraint(equalTo: customVC.view.centerYAnchor)
322
+ ])
323
+
324
+ dialogMessage.setValue(customVC, forKey: "contentViewController")
325
+
326
+ viewController.present(dialogMessage, animated: true, completion: nil)
327
+
328
+ let asset = AVAsset(url: inputFile)
329
+ guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
330
+ dialogMessage.dismiss(animated: true)
331
+ let eventPayload: [String: Any] = ["message": "Failed to create export session"]
332
+ self.emitEventToJS("onError", eventData: eventPayload)
333
+ return
334
+ }
335
+
336
+ if FileManager.default.fileExists(atPath: outputURL.path) {
337
+ try? FileManager.default.removeItem(at: outputURL)
338
+ }
339
+
340
+ exportSession.outputURL = outputURL
341
+ exportSession.outputFileType = .mp4
342
+ let startCMTime = CMTime(seconds: startTime, preferredTimescale: 600)
343
+ let durationCMTime = CMTime(seconds: max(0.1, (endTime - startTime)), preferredTimescale: 600)
344
+ exportSession.timeRange = CMTimeRange(start: startCMTime, duration: durationCMTime)
345
+
346
+ var timer: Timer?
347
+ timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
348
+ DispatchQueue.main.async {
349
+ progressView.setProgress(exportSession.progress, animated: true)
350
+ }
351
+ }
352
+
353
+ exportSession.exportAsynchronously {
354
+ DispatchQueue.main.async {
355
+ timer?.invalidate()
356
+ timer = nil
357
+ let _ = self.deleteFile(url: inputFile)
358
+
359
+ if exportSession.status == .completed {
360
+ let outputPath = outputURL.path
361
+ let eventPayload: [String: Any] = ["outputPath": outputPath]
362
+ self.emitEventToJS("onFinishTrimming", eventData: eventPayload)
363
+
364
+ if self.saveToPhoto {
365
+ PHPhotoLibrary.requestAuthorization { status in
366
+ guard status == .authorized else { return }
367
+ PHPhotoLibrary.shared().performChanges({
368
+ let request = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL)
369
+ request?.creationDate = Date()
370
+ })
371
+ }
372
+ }
373
+ } else {
374
+ let err = exportSession.error?.localizedDescription ?? "Export failed"
375
+ let eventPayload: [String: Any] = ["message": err]
376
+ self.emitEventToJS("onError", eventData: eventPayload)
377
+ }
378
+
379
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
380
+ dialogMessage.dismiss(animated: false)
381
+ viewController.dismiss(animated: true, completion: {
382
+ self.emitEventToJS("onHide", eventData: nil)
383
+ self.isShowing = false
384
+ })
385
+ }
386
+ }
387
+ }
388
+ }
389
+ }
@@ -0,0 +1,10 @@
1
+ //
2
+ // VideoTrim-Bridging-Header.h
3
+ // Shakebug
4
+ //
5
+ // Created by ideveloper2 on 26/04/24.
6
+ // Copyright © 2024 Facebook. All rights reserved.
7
+ //
8
+
9
+ #import <React/RCTBridgeModule.h>
10
+ #import <React/RCTViewManager.h>
@@ -0,0 +1,23 @@
1
+ //
2
+ // VideoTrim.m
3
+ // Shakebug
4
+ //
5
+ // Created by ideveloper2 on 26/04/24.
6
+ // Copyright © 2024 Facebook. All rights reserved.
7
+ //
8
+
9
+ #import <React/RCTBridgeModule.h>
10
+ #import <React/RCTEventEmitter.h>
11
+
12
+ @interface RCT_EXTERN_MODULE(ShakebugSDKVideoTrim, RCTEventEmitter)
13
+
14
+ RCT_EXTERN_METHOD(showEditor:(NSString*)uri withConfig:(NSDictionary *)config)
15
+ RCT_EXTERN_METHOD(isValidVideo:(NSString*)uri withResolver:(RCTPromiseResolveBlock)resolve
16
+ withRejecter:(RCTPromiseRejectBlock)reject)
17
+ RCT_EXTERN_METHOD(listFiles:(RCTPromiseResolveBlock)resolve
18
+ withRejecter:(RCTPromiseRejectBlock)reject)
19
+ RCT_EXTERN_METHOD(cleanFiles:(RCTPromiseResolveBlock)resolve
20
+ withRejecter:(RCTPromiseRejectBlock)reject)
21
+ RCT_EXTERN_METHOD(deleteFile:(NSString*)uri withResolver:(RCTPromiseResolveBlock)resolve
22
+ withRejecter:(RCTPromiseRejectBlock)reject)
23
+ @end