react-native-frame-capture 1.0.1

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 (77) hide show
  1. package/FrameCapture.podspec +21 -0
  2. package/LICENSE +20 -0
  3. package/README.md +158 -0
  4. package/android/build.gradle +77 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +20 -0
  7. package/android/src/main/java/com/framecapture/CaptureManager.kt +831 -0
  8. package/android/src/main/java/com/framecapture/Constants.kt +196 -0
  9. package/android/src/main/java/com/framecapture/ErrorHandler.kt +165 -0
  10. package/android/src/main/java/com/framecapture/FrameCaptureModule.kt +653 -0
  11. package/android/src/main/java/com/framecapture/FrameCapturePackage.kt +32 -0
  12. package/android/src/main/java/com/framecapture/OverlayRenderer.kt +423 -0
  13. package/android/src/main/java/com/framecapture/PermissionHandler.kt +150 -0
  14. package/android/src/main/java/com/framecapture/ScreenCaptureService.kt +366 -0
  15. package/android/src/main/java/com/framecapture/StorageManager.kt +221 -0
  16. package/android/src/main/java/com/framecapture/capture/BitmapProcessor.kt +157 -0
  17. package/android/src/main/java/com/framecapture/capture/CaptureEventEmitter.kt +120 -0
  18. package/android/src/main/java/com/framecapture/models/CaptureModels.kt +302 -0
  19. package/android/src/main/java/com/framecapture/models/EnumsAndExtensions.kt +60 -0
  20. package/android/src/main/java/com/framecapture/models/OverlayModels.kt +154 -0
  21. package/android/src/main/java/com/framecapture/service/CaptureNotificationManager.kt +286 -0
  22. package/android/src/main/java/com/framecapture/storage/StorageStrategies.kt +317 -0
  23. package/android/src/main/java/com/framecapture/utils/ValidationUtils.kt +379 -0
  24. package/app.plugin.js +1 -0
  25. package/ios/FrameCapture.h +5 -0
  26. package/ios/FrameCapture.mm +21 -0
  27. package/lib/module/NativeFrameCapture.js +24 -0
  28. package/lib/module/NativeFrameCapture.js.map +1 -0
  29. package/lib/module/api.js +146 -0
  30. package/lib/module/api.js.map +1 -0
  31. package/lib/module/constants.js +67 -0
  32. package/lib/module/constants.js.map +1 -0
  33. package/lib/module/errors.js +19 -0
  34. package/lib/module/errors.js.map +1 -0
  35. package/lib/module/events.js +58 -0
  36. package/lib/module/events.js.map +1 -0
  37. package/lib/module/index.js +24 -0
  38. package/lib/module/index.js.map +1 -0
  39. package/lib/module/normalize.js +51 -0
  40. package/lib/module/normalize.js.map +1 -0
  41. package/lib/module/package.json +1 -0
  42. package/lib/module/types.js +165 -0
  43. package/lib/module/types.js.map +1 -0
  44. package/lib/module/validation.js +190 -0
  45. package/lib/module/validation.js.map +1 -0
  46. package/lib/typescript/package.json +1 -0
  47. package/lib/typescript/plugin/src/index.d.ts +4 -0
  48. package/lib/typescript/plugin/src/index.d.ts.map +1 -0
  49. package/lib/typescript/src/NativeFrameCapture.d.ts +75 -0
  50. package/lib/typescript/src/NativeFrameCapture.d.ts.map +1 -0
  51. package/lib/typescript/src/api.d.ts +66 -0
  52. package/lib/typescript/src/api.d.ts.map +1 -0
  53. package/lib/typescript/src/constants.d.ts +41 -0
  54. package/lib/typescript/src/constants.d.ts.map +1 -0
  55. package/lib/typescript/src/errors.d.ts +14 -0
  56. package/lib/typescript/src/errors.d.ts.map +1 -0
  57. package/lib/typescript/src/events.d.ts +30 -0
  58. package/lib/typescript/src/events.d.ts.map +1 -0
  59. package/lib/typescript/src/index.d.ts +12 -0
  60. package/lib/typescript/src/index.d.ts.map +1 -0
  61. package/lib/typescript/src/normalize.d.ts +43 -0
  62. package/lib/typescript/src/normalize.d.ts.map +1 -0
  63. package/lib/typescript/src/types.d.ts +247 -0
  64. package/lib/typescript/src/types.d.ts.map +1 -0
  65. package/lib/typescript/src/validation.d.ts +15 -0
  66. package/lib/typescript/src/validation.d.ts.map +1 -0
  67. package/package.json +196 -0
  68. package/plugin/build/index.js +48 -0
  69. package/src/NativeFrameCapture.ts +86 -0
  70. package/src/api.ts +189 -0
  71. package/src/constants.ts +69 -0
  72. package/src/errors.ts +21 -0
  73. package/src/events.ts +61 -0
  74. package/src/index.tsx +31 -0
  75. package/src/normalize.ts +81 -0
  76. package/src/types.ts +327 -0
  77. package/src/validation.ts +321 -0
@@ -0,0 +1,154 @@
1
+ package com.framecapture.models
2
+
3
+ import com.facebook.react.bridge.ReadableMap
4
+
5
+ /**
6
+ * Overlay position - either preset or custom coordinates
7
+ */
8
+ sealed class OverlayPosition {
9
+ data class Preset(val value: String) : OverlayPosition()
10
+ data class Coordinates(val x: Float, val y: Float, val unit: String = com.framecapture.Constants.POSITION_UNIT_PIXELS) : OverlayPosition()
11
+
12
+ companion object {
13
+ fun fromReadableMap(map: ReadableMap?): OverlayPosition? {
14
+ if (map == null) return null
15
+
16
+ return try {
17
+ val preset = map.getString("preset")
18
+ if (preset != null) Preset(preset) else null
19
+ } catch (e: Exception) {
20
+ Coordinates(
21
+ x = if (map.hasKey("x")) map.getDouble("x").toFloat() else 0f,
22
+ y = if (map.hasKey("y")) map.getDouble("y").toFloat() else 0f,
23
+ unit = if (map.hasKey("unit")) map.getString("unit") ?: com.framecapture.Constants.POSITION_UNIT_PIXELS else com.framecapture.Constants.POSITION_UNIT_PIXELS
24
+ )
25
+ }
26
+ }
27
+
28
+ fun fromString(preset: String): OverlayPosition {
29
+ return Preset(preset)
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Text style configuration
36
+ */
37
+ data class TextStyle(
38
+ val fontSize: Int = com.framecapture.Constants.TEXT_DEFAULT_FONT_SIZE,
39
+ val color: String = com.framecapture.Constants.TEXT_DEFAULT_COLOR,
40
+ val backgroundColor: String = com.framecapture.Constants.TEXT_DEFAULT_BACKGROUND_COLOR,
41
+ val padding: Int = com.framecapture.Constants.TEXT_DEFAULT_PADDING,
42
+ val fontWeight: String = com.framecapture.Constants.TEXT_WEIGHT_NORMAL,
43
+ val textAlign: String = com.framecapture.Constants.TEXT_ALIGN_LEFT
44
+ ) {
45
+ companion object {
46
+ fun fromReadableMap(map: ReadableMap?): TextStyle {
47
+ if (map == null) return TextStyle()
48
+
49
+ val bgColor = map.getStringOrDefault("backgroundColor", com.framecapture.Constants.TEXT_DEFAULT_BACKGROUND_COLOR)
50
+
51
+ return TextStyle(
52
+ fontSize = map.getIntOrDefault("fontSize", com.framecapture.Constants.TEXT_DEFAULT_FONT_SIZE),
53
+ color = map.getStringOrDefault("color", com.framecapture.Constants.TEXT_DEFAULT_COLOR),
54
+ backgroundColor = bgColor,
55
+ padding = map.getIntOrDefault("padding", com.framecapture.Constants.TEXT_DEFAULT_PADDING),
56
+ fontWeight = map.getStringOrDefault("fontWeight", com.framecapture.Constants.TEXT_WEIGHT_NORMAL),
57
+ textAlign = map.getStringOrDefault("textAlign", com.framecapture.Constants.TEXT_ALIGN_LEFT)
58
+ )
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Image size configuration
65
+ */
66
+ data class ImageSize(
67
+ val width: Int,
68
+ val height: Int
69
+ ) {
70
+ companion object {
71
+ fun fromReadableMap(map: ReadableMap?): ImageSize? {
72
+ if (map == null) return null
73
+
74
+ return ImageSize(
75
+ width = if (map.hasKey("width")) map.getInt("width") else return null,
76
+ height = if (map.hasKey("height")) map.getInt("height") else return null
77
+ )
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Base overlay configuration
84
+ */
85
+ sealed class OverlayConfig {
86
+ abstract val type: String
87
+ abstract val position: OverlayPosition
88
+
89
+ /**
90
+ * Text overlay configuration
91
+ */
92
+ data class Text(
93
+ override val type: String = com.framecapture.Constants.OVERLAY_TYPE_TEXT,
94
+ val content: String,
95
+ override val position: OverlayPosition,
96
+ val style: TextStyle = TextStyle()
97
+ ) : OverlayConfig()
98
+
99
+ /**
100
+ * Image overlay configuration
101
+ */
102
+ data class Image(
103
+ override val type: String = com.framecapture.Constants.OVERLAY_TYPE_IMAGE,
104
+ val source: String,
105
+ override val position: OverlayPosition,
106
+ val size: ImageSize? = null,
107
+ val opacity: Float = com.framecapture.Constants.IMAGE_DEFAULT_OPACITY
108
+ ) : OverlayConfig()
109
+
110
+ companion object {
111
+ fun fromReadableMap(map: ReadableMap): OverlayConfig? {
112
+ val type = map.getStringOrNull("type") ?: return null
113
+
114
+ return when (type) {
115
+ com.framecapture.Constants.OVERLAY_TYPE_TEXT -> {
116
+ val content = map.getStringOrNull("content") ?: return null
117
+ val position = parsePositionFromMap(map) ?: return null
118
+ val style = TextStyle.fromReadableMap(map.getMap("style"))
119
+
120
+ Text(content = content, position = position, style = style)
121
+ }
122
+ com.framecapture.Constants.OVERLAY_TYPE_IMAGE -> {
123
+ val source = map.getStringOrNull("source") ?: return null
124
+ val position = parsePositionFromMap(map) ?: return null
125
+ val size = ImageSize.fromReadableMap(map.getMap("size"))
126
+ val opacity = if (map.hasKey("opacity")) map.getDouble("opacity").toFloat() else com.framecapture.Constants.IMAGE_DEFAULT_OPACITY
127
+
128
+ Image(source = source, position = position, size = size, opacity = opacity)
129
+ }
130
+ else -> null
131
+ }
132
+ }
133
+
134
+ private fun parsePositionFromMap(map: ReadableMap): OverlayPosition? {
135
+ if (!map.hasKey("position")) return null
136
+
137
+ return try {
138
+ val positionString = map.getString("position")
139
+ if (positionString != null) {
140
+ OverlayPosition.fromString(positionString)
141
+ } else {
142
+ null
143
+ }
144
+ } catch (e: Exception) {
145
+ try {
146
+ val positionMap = map.getMap("position")
147
+ OverlayPosition.fromReadableMap(positionMap)
148
+ } catch (e2: Exception) {
149
+ null
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,286 @@
1
+ package com.framecapture.service
2
+
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.app.PendingIntent
7
+ import android.app.Service
8
+ import android.content.Context
9
+ import android.content.Intent
10
+ import android.os.Build
11
+ import androidx.core.app.NotificationCompat
12
+ import com.framecapture.Constants
13
+ import com.framecapture.ScreenCaptureService
14
+ import com.framecapture.models.NotificationOptions
15
+
16
+ /**
17
+ * Manages notification creation, updates, and styling for the capture service
18
+ *
19
+ * Handles all notification-related operations for the foreground service:
20
+ * - Notification channel creation (Android 8.0+)
21
+ * - Custom styling (colors, icons, priority)
22
+ * - Action buttons (pause/resume/stop)
23
+ * - Frame count updates with template substitution
24
+ * - Paused state notifications
25
+ *
26
+ * Supports full customization via NotificationOptions.
27
+ */
28
+ class CaptureNotificationManager(
29
+ private val service: Service,
30
+ private val notificationOptions: NotificationOptions
31
+ ) {
32
+
33
+ companion object {
34
+ const val NOTIFICATION_ID = Constants.NOTIFICATION_ID
35
+ }
36
+
37
+ /**
38
+ * Resolves an icon resource name to a resource ID
39
+ * Falls back to default icon if resource not found or null
40
+ */
41
+ private fun getIconResource(iconName: String?): Int {
42
+ if (iconName == null) {
43
+ return android.R.drawable.ic_menu_camera
44
+ }
45
+
46
+ try {
47
+ val resourceId = service.applicationContext.resources.getIdentifier(
48
+ iconName,
49
+ Constants.RESOURCE_TYPE_DRAWABLE,
50
+ service.applicationContext.packageName
51
+ )
52
+
53
+ if (resourceId == 0) {
54
+ return android.R.drawable.ic_menu_camera
55
+ }
56
+
57
+ return resourceId
58
+ } catch (e: Exception) {
59
+ return android.R.drawable.ic_menu_camera
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Parses a color hex string to a color integer
65
+ * Returns null if parsing fails or input is null
66
+ */
67
+ private fun parseColor(colorString: String?): Int? {
68
+ if (colorString == null) return null
69
+
70
+ try {
71
+ return android.graphics.Color.parseColor(colorString)
72
+ } catch (e: Exception) {
73
+ return null
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Maps a priority string to a NotificationCompat priority constant
79
+ * Supports "low", "default", "high" (case-insensitive)
80
+ */
81
+ private fun getNotificationPriority(priority: String): Int {
82
+ return when (priority.lowercase()) {
83
+ Constants.NOTIFICATION_PRIORITY_HIGH -> NotificationCompat.PRIORITY_HIGH
84
+ Constants.NOTIFICATION_PRIORITY_DEFAULT -> NotificationCompat.PRIORITY_DEFAULT
85
+ Constants.NOTIFICATION_PRIORITY_LOW -> NotificationCompat.PRIORITY_LOW
86
+ else -> NotificationCompat.PRIORITY_LOW
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Formats a description template by replacing {frameCount} placeholder with actual count
92
+ */
93
+ private fun formatDescription(template: String, frameCount: Int): String {
94
+ return template.replace(Constants.NOTIFICATION_TEMPLATE_FRAME_COUNT, frameCount.toString())
95
+ }
96
+
97
+ /**
98
+ * Generates a unique channel ID based on notification options
99
+ * Returns default CHANNEL_ID for default settings, or hash-based ID for custom channels
100
+ */
101
+ private fun getChannelId(): String {
102
+ return if (notificationOptions.channelName == Constants.CHANNEL_NAME) {
103
+ Constants.CHANNEL_ID
104
+ } else {
105
+ "${Constants.CUSTOM_CHANNEL_ID_PREFIX}${notificationOptions.channelName.hashCode()}"
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Creates notification channel for API 26+
111
+ * Uses custom channel settings from notificationOptions
112
+ */
113
+ fun createNotificationChannel() {
114
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
115
+ val channelId = getChannelId()
116
+
117
+ val channel = NotificationChannel(
118
+ channelId,
119
+ notificationOptions.channelName,
120
+ NotificationManager.IMPORTANCE_LOW
121
+ ).apply {
122
+ description = notificationOptions.channelDescription
123
+ setShowBadge(false)
124
+ }
125
+
126
+ val notificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
127
+ notificationManager.createNotificationChannel(channel)
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Creates a reusable notification builder with custom styling applied
133
+ */
134
+ private fun createNotificationBuilder(): NotificationCompat.Builder {
135
+ val builder = NotificationCompat.Builder(service, getChannelId())
136
+ .setSmallIcon(getIconResource(notificationOptions.smallIcon))
137
+ .setPriority(getNotificationPriority(notificationOptions.priority))
138
+ .setOngoing(true)
139
+
140
+ // Apply color if provided
141
+ parseColor(notificationOptions.color)?.let {
142
+ builder.setColor(it)
143
+ }
144
+
145
+ // Apply large icon if provided
146
+ notificationOptions.icon?.let { iconName ->
147
+ try {
148
+ val iconRes = getIconResource(iconName)
149
+ val bitmap = android.graphics.BitmapFactory.decodeResource(service.resources, iconRes)
150
+ if (bitmap != null) {
151
+ builder.setLargeIcon(bitmap)
152
+ }
153
+ } catch (e: Exception) {
154
+ // Silently fail - large icon is optional
155
+ }
156
+ }
157
+
158
+ return builder
159
+ }
160
+
161
+ /**
162
+ * Creates a PendingIntent for service actions
163
+ */
164
+ private fun createActionPendingIntent(action: String, requestCode: Int): PendingIntent {
165
+ val intent = Intent(service, ScreenCaptureService::class.java).apply {
166
+ this.action = action
167
+ }
168
+
169
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
170
+ PendingIntent.getService(
171
+ service,
172
+ requestCode,
173
+ intent,
174
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
175
+ )
176
+ } else {
177
+ PendingIntent.getService(
178
+ service,
179
+ requestCode,
180
+ intent,
181
+ PendingIntent.FLAG_UPDATE_CURRENT
182
+ )
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Creates the initial foreground service notification
188
+ */
189
+ fun createNotification(): Notification {
190
+ val builder = createNotificationBuilder()
191
+ .setContentTitle(notificationOptions.title)
192
+ .setContentText(notificationOptions.description)
193
+
194
+ // Add pause action if enabled
195
+ if (notificationOptions.showPauseAction) {
196
+ builder.addAction(
197
+ android.R.drawable.ic_media_pause,
198
+ Constants.NOTIFICATION_ACTION_PAUSE,
199
+ createActionPendingIntent(Constants.ACTION_PAUSE_CAPTURE, Constants.NOTIFICATION_REQUEST_CODE_PAUSE_RESUME)
200
+ )
201
+ }
202
+
203
+ // Add stop action if enabled
204
+ if (notificationOptions.showStopAction) {
205
+ builder.addAction(
206
+ android.R.drawable.ic_menu_close_clear_cancel,
207
+ Constants.NOTIFICATION_ACTION_STOP,
208
+ createActionPendingIntent(Constants.ACTION_STOP_CAPTURE, Constants.NOTIFICATION_REQUEST_CODE_STOP)
209
+ )
210
+ }
211
+
212
+ return builder.build()
213
+ }
214
+
215
+ /**
216
+ * Updates the notification with current frame count progress
217
+ */
218
+ fun updateNotification(frameCount: Int) {
219
+ try {
220
+ val builder = createNotificationBuilder()
221
+ .setContentTitle(notificationOptions.title)
222
+ .setContentText(formatDescription(notificationOptions.description, frameCount))
223
+
224
+ // Add pause action if enabled
225
+ if (notificationOptions.showPauseAction) {
226
+ builder.addAction(
227
+ android.R.drawable.ic_media_pause,
228
+ Constants.NOTIFICATION_ACTION_PAUSE,
229
+ createActionPendingIntent(Constants.ACTION_PAUSE_CAPTURE, Constants.NOTIFICATION_REQUEST_CODE_PAUSE_RESUME)
230
+ )
231
+ }
232
+
233
+ // Add stop action if enabled
234
+ if (notificationOptions.showStopAction) {
235
+ builder.addAction(
236
+ android.R.drawable.ic_menu_close_clear_cancel,
237
+ Constants.NOTIFICATION_ACTION_STOP,
238
+ createActionPendingIntent(Constants.ACTION_STOP_CAPTURE, Constants.NOTIFICATION_REQUEST_CODE_STOP)
239
+ )
240
+ }
241
+
242
+ val notification = builder.build()
243
+ val notificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
244
+ notificationManager.notify(NOTIFICATION_ID, notification)
245
+
246
+ } catch (e: Exception) {
247
+ // Silently fail - notification update is not critical
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Updates the notification to show paused state
253
+ */
254
+ fun updateNotificationPaused(frameCount: Int) {
255
+ try {
256
+ val builder = createNotificationBuilder()
257
+ .setContentTitle(notificationOptions.pausedTitle)
258
+ .setContentText(formatDescription(notificationOptions.pausedDescription, frameCount))
259
+
260
+ // Add resume action if enabled
261
+ if (notificationOptions.showResumeAction) {
262
+ builder.addAction(
263
+ android.R.drawable.ic_media_play,
264
+ Constants.NOTIFICATION_ACTION_RESUME,
265
+ createActionPendingIntent(Constants.ACTION_RESUME_CAPTURE, Constants.NOTIFICATION_REQUEST_CODE_PAUSE_RESUME)
266
+ )
267
+ }
268
+
269
+ // Add stop action if enabled
270
+ if (notificationOptions.showStopAction) {
271
+ builder.addAction(
272
+ android.R.drawable.ic_menu_close_clear_cancel,
273
+ Constants.NOTIFICATION_ACTION_STOP,
274
+ createActionPendingIntent(Constants.ACTION_STOP_CAPTURE, Constants.NOTIFICATION_REQUEST_CODE_STOP)
275
+ )
276
+ }
277
+
278
+ val notification = builder.build()
279
+ val notificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
280
+ notificationManager.notify(NOTIFICATION_ID, notification)
281
+
282
+ } catch (e: Exception) {
283
+ // Silently fail - notification update is not critical
284
+ }
285
+ }
286
+ }