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,317 @@
1
+ package com.framecapture.storage
2
+
3
+ import android.content.ContentValues
4
+ import android.content.Context
5
+ import android.graphics.Bitmap
6
+ import android.net.Uri
7
+ import android.os.Build
8
+ import android.os.Environment
9
+ import android.provider.MediaStore
10
+ import android.util.Log
11
+ import com.framecapture.Constants
12
+ import com.framecapture.models.FrameInfo
13
+ import java.io.File
14
+ import java.io.FileOutputStream
15
+ import java.io.IOException
16
+
17
+ /**
18
+ * Implements different storage strategies for saving captured frames
19
+ *
20
+ * Provides multiple storage options with appropriate Android API handling:
21
+ * - Temporary storage: Cache directory (auto-cleaned on app restart)
22
+ * - App-specific storage: No permissions needed, cleaned on uninstall
23
+ * - Public storage: MediaStore API (Android 10+) for gallery visibility
24
+ * - Custom directory: User-specified path with validation
25
+ *
26
+ * Handles bitmap compression, file I/O, and error recovery (cleanup on failure).
27
+ */
28
+ class StorageStrategies(private val context: Context) {
29
+
30
+ companion object {
31
+ private const val TAG = "StorageStrategies"
32
+ }
33
+
34
+ /**
35
+ * Returns the app-specific pictures directory (no permissions needed)
36
+ */
37
+ fun getAppSpecificDirectory(): File {
38
+ val picturesDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
39
+ ?: context.filesDir // Fallback to internal storage
40
+
41
+ // Create directory if it doesn't exist
42
+ if (!picturesDir.exists()) {
43
+ picturesDir.mkdirs()
44
+ }
45
+
46
+ return picturesDir
47
+ }
48
+
49
+ /**
50
+ * Returns the temporary cache directory for frames when saveFrames is false
51
+ */
52
+ fun getTempDirectory(): File {
53
+ val tempDir = File(context.cacheDir, Constants.TEMP_FRAMES_DIRECTORY)
54
+ if (!tempDir.exists()) {
55
+ tempDir.mkdirs()
56
+ }
57
+ return tempDir
58
+ }
59
+
60
+ /**
61
+ * Returns the MIME type for the given format
62
+ */
63
+ fun getMimeType(format: String): String {
64
+ return when (format.lowercase()) {
65
+ Constants.FORMAT_EXTENSION_PNG -> Constants.MIME_TYPE_PNG
66
+ else -> Constants.MIME_TYPE_JPEG
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Returns the Bitmap.CompressFormat for the given format string
72
+ */
73
+ fun getCompressFormat(format: String): Bitmap.CompressFormat {
74
+ return when (format.lowercase()) {
75
+ Constants.FORMAT_EXTENSION_PNG -> Bitmap.CompressFormat.PNG
76
+ else -> Bitmap.CompressFormat.JPEG
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Saves frame to temporary cache directory (when saveFrames is false)
82
+ */
83
+ fun saveToTempDirectory(
84
+ bitmap: Bitmap,
85
+ sessionId: String,
86
+ filename: String,
87
+ format: Bitmap.CompressFormat,
88
+ quality: Int
89
+ ): FrameInfo {
90
+ // Create session-specific temp directory
91
+ val tempBaseDir = getTempDirectory()
92
+ val sessionTempDir = File(tempBaseDir, sessionId)
93
+
94
+ if (!sessionTempDir.exists()) {
95
+ if (!sessionTempDir.mkdirs()) {
96
+ throw IOException("Failed to create temp directory: ${sessionTempDir.absolutePath}")
97
+ }
98
+ }
99
+
100
+ val file = File(sessionTempDir, filename)
101
+ return saveToFile(file, bitmap, format, quality)
102
+ }
103
+
104
+ /**
105
+ * Saves frame to a custom directory specified by the user
106
+ */
107
+ fun saveToCustomDirectory(
108
+ bitmap: Bitmap,
109
+ customPath: String,
110
+ filename: String,
111
+ format: Bitmap.CompressFormat,
112
+ quality: Int
113
+ ): FrameInfo {
114
+ val directory = File(customPath)
115
+
116
+ // Validate directory
117
+ if (!directory.exists()) {
118
+ if (!directory.mkdirs()) {
119
+ throw IOException("Failed to create custom directory: $customPath")
120
+ }
121
+ }
122
+
123
+ if (!directory.isDirectory) {
124
+ throw IOException("Custom path is not a directory: $customPath")
125
+ }
126
+
127
+ if (!directory.canWrite()) {
128
+ throw IOException("No write permission for custom directory: $customPath")
129
+ }
130
+
131
+ val file = File(directory, filename)
132
+ return saveToFile(file, bitmap, format, quality)
133
+ }
134
+
135
+ /**
136
+ * Saves frame to MediaStore (API 29+) for gallery visibility with session folder
137
+ */
138
+ fun saveToMediaStore(
139
+ bitmap: Bitmap,
140
+ sessionId: String,
141
+ filename: String,
142
+ formatString: String,
143
+ quality: Int
144
+ ): FrameInfo {
145
+ val mimeType = getMimeType(formatString)
146
+ val format = getCompressFormat(formatString)
147
+
148
+ // Create session-specific subfolder in Pictures
149
+ val relativePath = "${Environment.DIRECTORY_PICTURES}/$sessionId"
150
+
151
+ val contentValues = ContentValues().apply {
152
+ put(MediaStore.Images.Media.DISPLAY_NAME, filename)
153
+ put(MediaStore.Images.Media.MIME_TYPE, mimeType)
154
+ put(MediaStore.Images.Media.RELATIVE_PATH, relativePath)
155
+ put(MediaStore.Images.Media.IS_PENDING, 1)
156
+ }
157
+
158
+ val uri = context.contentResolver.insert(
159
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
160
+ contentValues
161
+ ) ?: throw IOException("Failed to create MediaStore entry")
162
+
163
+ try {
164
+ context.contentResolver.openOutputStream(uri)?.use { outputStream ->
165
+ if (!bitmap.compress(format, quality, outputStream)) {
166
+ throw IOException("Failed to compress bitmap to MediaStore")
167
+ }
168
+ } ?: throw IOException("Failed to open output stream for MediaStore")
169
+
170
+ // Mark as not pending
171
+ contentValues.clear()
172
+ contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
173
+ context.contentResolver.update(uri, contentValues, null, null)
174
+
175
+ // Get file size and actual file path from MediaStore
176
+ val fileSize = getMediaStoreFileSize(uri)
177
+ val actualFilePath = getMediaStoreFilePath(uri) ?: uri.toString()
178
+
179
+ return FrameInfo(
180
+ filePath = actualFilePath,
181
+ fileSize = fileSize,
182
+ timestamp = System.currentTimeMillis()
183
+ )
184
+ } catch (e: Exception) {
185
+ // Clean up on error
186
+ context.contentResolver.delete(uri, null, null)
187
+ throw IOException("Failed to save frame to MediaStore: ${e.message}", e)
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Saves frame to app-specific directory with session folder (default, no permissions needed)
193
+ */
194
+ fun saveToAppSpecificDirectory(
195
+ bitmap: Bitmap,
196
+ sessionId: String,
197
+ filename: String,
198
+ format: Bitmap.CompressFormat,
199
+ quality: Int
200
+ ): FrameInfo {
201
+ // Create session-specific directory
202
+ val baseDirectory = getAppSpecificDirectory()
203
+ val sessionDirectory = File(baseDirectory, sessionId)
204
+
205
+ if (!sessionDirectory.exists()) {
206
+ if (!sessionDirectory.mkdirs()) {
207
+ throw IOException("Failed to create session directory: ${sessionDirectory.absolutePath}")
208
+ }
209
+ }
210
+
211
+ val file = File(sessionDirectory, filename)
212
+ return saveToFile(file, bitmap, format, quality)
213
+ }
214
+
215
+ /**
216
+ * Saves bitmap to a file
217
+ */
218
+ fun saveToFile(
219
+ file: File,
220
+ bitmap: Bitmap,
221
+ format: Bitmap.CompressFormat,
222
+ quality: Int
223
+ ): FrameInfo {
224
+ try {
225
+ FileOutputStream(file).use { outputStream ->
226
+ if (!bitmap.compress(format, quality, outputStream)) {
227
+ throw IOException("Failed to compress bitmap to file")
228
+ }
229
+ outputStream.flush()
230
+ }
231
+
232
+ return FrameInfo(
233
+ filePath = file.absolutePath,
234
+ fileSize = file.length(),
235
+ timestamp = System.currentTimeMillis()
236
+ )
237
+ } catch (e: Exception) {
238
+ // Clean up partial file on error
239
+ if (file.exists()) {
240
+ file.delete()
241
+ }
242
+ throw IOException("Failed to save frame to file: ${e.message}", e)
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Gets the file size from MediaStore URI (approximate)
248
+ */
249
+ private fun getMediaStoreFileSize(uri: Uri): Long {
250
+ return try {
251
+ context.contentResolver.query(
252
+ uri,
253
+ arrayOf(MediaStore.Images.Media.SIZE),
254
+ null,
255
+ null,
256
+ null
257
+ )?.use { cursor ->
258
+ if (cursor.moveToFirst()) {
259
+ val sizeIndex = cursor.getColumnIndex(MediaStore.Images.Media.SIZE)
260
+ if (sizeIndex >= 0) {
261
+ cursor.getLong(sizeIndex)
262
+ } else {
263
+ 0L
264
+ }
265
+ } else {
266
+ 0L
267
+ }
268
+ } ?: 0L
269
+ } catch (e: Exception) {
270
+ 0L
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Gets the actual file path from MediaStore URI
276
+ * Returns the absolute file path that can be used with file:// scheme
277
+ */
278
+ private fun getMediaStoreFilePath(uri: Uri): String? {
279
+ return try {
280
+ context.contentResolver.query(
281
+ uri,
282
+ arrayOf(MediaStore.Images.Media.DATA),
283
+ null,
284
+ null,
285
+ null
286
+ )?.use { cursor ->
287
+ if (cursor.moveToFirst()) {
288
+ val dataIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA)
289
+ if (dataIndex >= 0) {
290
+ cursor.getString(dataIndex)
291
+ } else {
292
+ null
293
+ }
294
+ } else {
295
+ null
296
+ }
297
+ }
298
+ } catch (e: Exception) {
299
+ null
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Cleans up all temporary frame files
305
+ * Called on app startup and can be called manually
306
+ */
307
+ fun cleanupAllTempFiles() {
308
+ try {
309
+ val tempDir = File(context.cacheDir, Constants.TEMP_FRAMES_DIRECTORY)
310
+ if (tempDir.exists()) {
311
+ tempDir.deleteRecursively()
312
+ }
313
+ } catch (e: Exception) {
314
+ Log.e(TAG, "Failed to cleanup temp files: ${e.message}", e)
315
+ }
316
+ }
317
+ }
@@ -0,0 +1,379 @@
1
+ package com.framecapture.utils
2
+
3
+ import com.framecapture.models.CaptureOptions
4
+ import com.framecapture.models.OverlayConfig
5
+ import com.framecapture.models.OverlayPosition
6
+ import java.io.File
7
+
8
+ /**
9
+ * Sealed class representing validation result
10
+ *
11
+ * Either Success (validation passed) or Error (with list of error messages)
12
+ */
13
+ sealed class ValidationResult {
14
+ object Success : ValidationResult()
15
+ data class Error(val messages: List<String>) : ValidationResult()
16
+ }
17
+
18
+ /**
19
+ * Validation utilities for capture options
20
+ *
21
+ * Provides comprehensive validation for all capture configuration options:
22
+ * - Capture settings (interval, quality, format, resolution)
23
+ * - Storage configuration (output directory, permissions)
24
+ * - Overlay configuration (text, images, positions, styling)
25
+ * - Color format validation (hex colors)
26
+ *
27
+ * Returns detailed error messages for debugging and user feedback.
28
+ */
29
+ object ValidationUtils {
30
+
31
+ // Import validation constants from Constants object
32
+ // Support both "jpeg" and "jpg" for compatibility
33
+ private val VALID_FORMATS = setOf(com.framecapture.Constants.FORMAT_EXTENSION_PNG, com.framecapture.Constants.FORMAT_EXTENSION_JPEG, "jpeg")
34
+ private val VALID_OVERLAY_TYPES = setOf(com.framecapture.Constants.OVERLAY_TYPE_TEXT, com.framecapture.Constants.OVERLAY_TYPE_IMAGE)
35
+ private val VALID_POSITION_PRESETS = setOf(
36
+ com.framecapture.Constants.POSITION_TOP_LEFT,
37
+ com.framecapture.Constants.POSITION_TOP_RIGHT,
38
+ com.framecapture.Constants.POSITION_BOTTOM_LEFT,
39
+ com.framecapture.Constants.POSITION_BOTTOM_RIGHT,
40
+ com.framecapture.Constants.POSITION_CENTER
41
+ )
42
+
43
+ /**
44
+ * Validates capture options comprehensively
45
+ *
46
+ * Checks all configuration values against constraints and returns detailed
47
+ * error messages for any validation failures.
48
+ *
49
+ * @param options The CaptureOptions to validate
50
+ * @return ValidationResult.Success if valid, ValidationResult.Error with messages if invalid
51
+ */
52
+ fun validateOptions(options: CaptureOptions): ValidationResult {
53
+ val errors = mutableListOf<String>()
54
+
55
+ // Validate interval
56
+ if (options.interval < com.framecapture.Constants.MIN_INTERVAL || options.interval > com.framecapture.Constants.MAX_INTERVAL) {
57
+ errors.add(
58
+ "interval must be between ${com.framecapture.Constants.MIN_INTERVAL} and ${com.framecapture.Constants.MAX_INTERVAL} milliseconds. " +
59
+ "Provided: ${options.interval}ms"
60
+ )
61
+ }
62
+
63
+ // Validate quality
64
+ if (options.quality < com.framecapture.Constants.MIN_QUALITY || options.quality > com.framecapture.Constants.MAX_QUALITY) {
65
+ errors.add(
66
+ "quality must be between ${com.framecapture.Constants.MIN_QUALITY} and ${com.framecapture.Constants.MAX_QUALITY}. " +
67
+ "Provided: ${options.quality}"
68
+ )
69
+ }
70
+
71
+ // Validate format
72
+ if (options.format !in VALID_FORMATS) {
73
+ errors.add(
74
+ "format must be one of: ${VALID_FORMATS.joinToString(", ")}. " +
75
+ "Provided: '${options.format}'"
76
+ )
77
+ }
78
+
79
+ // Validate output directory if provided
80
+ options.outputDirectory?.let { directory ->
81
+ val validationError = validateOutputDirectory(directory)
82
+ if (validationError != null) {
83
+ errors.add(validationError)
84
+ }
85
+ }
86
+
87
+ // Validate scale resolution if provided
88
+ options.scaleResolution?.let { scale ->
89
+ if (scale < com.framecapture.Constants.MIN_SCALE_RESOLUTION || scale > com.framecapture.Constants.MAX_SCALE_RESOLUTION) {
90
+ errors.add(
91
+ "scaleResolution must be between ${com.framecapture.Constants.MIN_SCALE_RESOLUTION} and ${com.framecapture.Constants.MAX_SCALE_RESOLUTION}. " +
92
+ "Provided: $scale"
93
+ )
94
+ }
95
+ }
96
+
97
+ // Validate overlays if provided
98
+ options.overlays?.let { overlays ->
99
+ overlays.forEachIndexed { index, overlay ->
100
+ val overlayErrors = validateOverlay(overlay, index)
101
+ errors.addAll(overlayErrors)
102
+ }
103
+ }
104
+
105
+ // Validate advanced config
106
+ errors.addAll(validateAdvancedConfig(options.advanced))
107
+
108
+ return if (errors.isEmpty()) {
109
+ ValidationResult.Success
110
+ } else {
111
+ ValidationResult.Error(errors)
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Validates advanced configuration
117
+ */
118
+ private fun validateAdvancedConfig(advanced: com.framecapture.models.AdvancedConfig): List<String> {
119
+ val errors = mutableListOf<String>()
120
+
121
+ // Validate storage config
122
+ if (advanced.storage.warningThreshold < 0) {
123
+ errors.add("advanced.storage.warningThreshold must be non-negative. Provided: ${advanced.storage.warningThreshold}")
124
+ }
125
+
126
+ // Validate file naming config
127
+ val fn = advanced.fileNaming
128
+
129
+ if (fn.prefix.isEmpty()) {
130
+ errors.add("advanced.fileNaming.prefix must not be empty")
131
+ }
132
+
133
+ if (fn.prefix.contains(Regex("[<>:\"/\\\\|?*]"))) {
134
+ errors.add("advanced.fileNaming.prefix contains invalid filename characters. Provided: '${fn.prefix}'")
135
+ }
136
+
137
+ if (fn.dateFormat.isEmpty()) {
138
+ errors.add("advanced.fileNaming.dateFormat must not be empty")
139
+ }
140
+
141
+ if (fn.framePadding < 1 || fn.framePadding > 10) {
142
+ errors.add("advanced.fileNaming.framePadding must be between 1 and 10. Provided: ${fn.framePadding}")
143
+ }
144
+
145
+ // Validate performance config
146
+ val perf = advanced.performance
147
+
148
+ if (perf.overlayCacheSize < 0) {
149
+ errors.add("advanced.performance.overlayCacheSize must be non-negative. Provided: ${perf.overlayCacheSize}")
150
+ }
151
+
152
+ if (perf.imageReaderBuffers < 1 || perf.imageReaderBuffers > 10) {
153
+ errors.add("advanced.performance.imageReaderBuffers must be between 1 and 10. Provided: ${perf.imageReaderBuffers}")
154
+ }
155
+
156
+ if (perf.executorShutdownTimeout < 100 || perf.executorShutdownTimeout > 30000) {
157
+ errors.add("advanced.performance.executorShutdownTimeout must be between 100 and 30000ms. Provided: ${perf.executorShutdownTimeout}")
158
+ }
159
+
160
+ if (perf.executorForcedShutdownTimeout < 100 || perf.executorForcedShutdownTimeout > 10000) {
161
+ errors.add("advanced.performance.executorForcedShutdownTimeout must be between 100 and 10000ms. Provided: ${perf.executorForcedShutdownTimeout}")
162
+ }
163
+
164
+ return errors
165
+ }
166
+
167
+ /**
168
+ * Validates a single overlay configuration
169
+ *
170
+ * @param overlay The overlay to validate
171
+ * @param index The index of the overlay in the array (for error messages)
172
+ * @return List of error messages (empty if valid)
173
+ */
174
+ private fun validateOverlay(overlay: OverlayConfig, index: Int): List<String> {
175
+ val errors = mutableListOf<String>()
176
+ val prefix = "overlays[$index]"
177
+
178
+ // Validate overlay type
179
+ if (overlay.type !in VALID_OVERLAY_TYPES) {
180
+ errors.add(
181
+ "$prefix: type must be one of: ${VALID_OVERLAY_TYPES.joinToString(", ")}. " +
182
+ "Provided: '${overlay.type}'"
183
+ )
184
+ }
185
+
186
+ // Validate based on overlay type
187
+ when (overlay) {
188
+ is OverlayConfig.Text -> {
189
+ // Validate text content
190
+ if (overlay.content.isBlank()) {
191
+ errors.add("$prefix: text overlay must have non-empty content")
192
+ }
193
+
194
+ // Validate text style colors
195
+ val colorErrors = validateTextColors(overlay.style.color, overlay.style.backgroundColor, prefix)
196
+ errors.addAll(colorErrors)
197
+ }
198
+ is OverlayConfig.Image -> {
199
+ // Validate image source
200
+ if (overlay.source.isBlank()) {
201
+ errors.add("$prefix: image overlay must have non-empty source")
202
+ }
203
+
204
+ // Validate opacity
205
+ if (overlay.opacity < com.framecapture.Constants.MIN_OVERLAY_OPACITY || overlay.opacity > com.framecapture.Constants.MAX_OVERLAY_OPACITY) {
206
+ errors.add(
207
+ "$prefix: opacity must be between ${com.framecapture.Constants.MIN_OVERLAY_OPACITY} and ${com.framecapture.Constants.MAX_OVERLAY_OPACITY}. " +
208
+ "Provided: ${overlay.opacity}"
209
+ )
210
+ }
211
+ }
212
+ }
213
+
214
+ // Validate position
215
+ val positionErrors = validatePosition(overlay.position, prefix)
216
+ errors.addAll(positionErrors)
217
+
218
+ return errors
219
+ }
220
+
221
+ /**
222
+ * Validates overlay position
223
+ *
224
+ * @param position The position to validate
225
+ * @param prefix Error message prefix
226
+ * @return List of error messages (empty if valid)
227
+ */
228
+ private fun validatePosition(position: OverlayPosition, prefix: String): List<String> {
229
+ val errors = mutableListOf<String>()
230
+
231
+ when (position) {
232
+ is OverlayPosition.Preset -> {
233
+ if (position.value !in VALID_POSITION_PRESETS) {
234
+ errors.add(
235
+ "$prefix.position: preset must be one of: ${VALID_POSITION_PRESETS.joinToString(", ")}. " +
236
+ "Provided: '${position.value}'"
237
+ )
238
+ }
239
+ }
240
+ is OverlayPosition.Coordinates -> {
241
+ // Validate unit
242
+ if (position.unit !in setOf(com.framecapture.Constants.POSITION_UNIT_PIXELS, com.framecapture.Constants.POSITION_UNIT_PERCENTAGE)) {
243
+ errors.add(
244
+ "$prefix.position: unit must be '${com.framecapture.Constants.POSITION_UNIT_PIXELS}' or '${com.framecapture.Constants.POSITION_UNIT_PERCENTAGE}'. " +
245
+ "Provided: '${position.unit}'"
246
+ )
247
+ }
248
+
249
+ // Validate percentage coordinates are in valid range
250
+ if (position.unit == com.framecapture.Constants.POSITION_UNIT_PERCENTAGE) {
251
+ if (position.x < 0f || position.x > 1f) {
252
+ errors.add(
253
+ "$prefix.position: x coordinate with percentage unit must be between 0.0 and 1.0. " +
254
+ "Provided: ${position.x}"
255
+ )
256
+ }
257
+ if (position.y < 0f || position.y > 1f) {
258
+ errors.add(
259
+ "$prefix.position: y coordinate with percentage unit must be between 0.0 and 1.0. " +
260
+ "Provided: ${position.y}"
261
+ )
262
+ }
263
+ }
264
+
265
+ // Validate pixel coordinates are non-negative
266
+ if (position.unit == com.framecapture.Constants.POSITION_UNIT_PIXELS) {
267
+ if (position.x < 0f) {
268
+ errors.add("$prefix.position: x coordinate cannot be negative. Provided: ${position.x}")
269
+ }
270
+ if (position.y < 0f) {
271
+ errors.add("$prefix.position: y coordinate cannot be negative. Provided: ${position.y}")
272
+ }
273
+ }
274
+ }
275
+ }
276
+
277
+ return errors
278
+ }
279
+
280
+ /**
281
+ * Validates text overlay colors
282
+ *
283
+ * @param color Text color
284
+ * @param backgroundColor Background color
285
+ * @param prefix Error message prefix
286
+ * @return List of error messages (empty if valid)
287
+ */
288
+ private fun validateTextColors(color: String, backgroundColor: String, prefix: String): List<String> {
289
+ val errors = mutableListOf<String>()
290
+
291
+ // Validate text color format
292
+ if (!isValidHexColor(color)) {
293
+ errors.add(
294
+ "$prefix.style.color: must be a valid hex color (e.g., '#FFFFFF', '#FFF', '#AARRGGBB'). " +
295
+ "Provided: '$color'"
296
+ )
297
+ }
298
+
299
+ // Validate background color format
300
+ if (backgroundColor.isNotEmpty() && !isValidHexColor(backgroundColor)) {
301
+ errors.add(
302
+ "$prefix.style.backgroundColor: must be a valid hex color (e.g., '#FFFFFF', '#FFF', '#AARRGGBB'). " +
303
+ "Provided: '$backgroundColor'"
304
+ )
305
+ }
306
+
307
+ return errors
308
+ }
309
+
310
+ /**
311
+ * Checks if a string is a valid hex color format
312
+ *
313
+ * @param color The color string to validate
314
+ * @return true if valid hex color, false otherwise
315
+ */
316
+ private fun isValidHexColor(color: String): Boolean {
317
+ if (!color.startsWith("#")) return false
318
+
319
+ val hex = color.substring(1)
320
+
321
+ // Valid formats: #RGB, #RRGGBB, #AARRGGBB
322
+ return when (hex.length) {
323
+ com.framecapture.Constants.HEX_COLOR_LENGTH_SHORT,
324
+ com.framecapture.Constants.HEX_COLOR_LENGTH_MEDIUM,
325
+ com.framecapture.Constants.HEX_COLOR_LENGTH_LONG -> hex.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' }
326
+ else -> false
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Validates output directory path
332
+ *
333
+ * @param directory The directory path to validate
334
+ * @return Error message if invalid, null if valid
335
+ */
336
+ private fun validateOutputDirectory(directory: String): String? {
337
+ if (directory.isBlank()) {
338
+ return "outputDirectory cannot be empty or blank"
339
+ }
340
+
341
+ try {
342
+ val file = File(directory)
343
+
344
+ // Check if path is absolute
345
+ if (!file.isAbsolute) {
346
+ return "outputDirectory must be an absolute path. Provided: '$directory'"
347
+ }
348
+
349
+ // Check if directory exists and is writable (if it exists)
350
+ if (file.exists()) {
351
+ if (!file.isDirectory) {
352
+ return "outputDirectory must be a directory, not a file. Provided: '$directory'"
353
+ }
354
+ if (!file.canWrite()) {
355
+ return "outputDirectory is not writable. Provided: '$directory'"
356
+ }
357
+ }
358
+ } catch (e: Exception) {
359
+ return "outputDirectory path is invalid: ${e.message}"
360
+ }
361
+
362
+ return null
363
+ }
364
+
365
+ /**
366
+ * Creates a user-friendly error message from validation result
367
+ *
368
+ * @param result The ValidationResult to format
369
+ * @return Formatted error message or null if validation succeeded
370
+ */
371
+ fun formatValidationError(result: ValidationResult): String? {
372
+ return when (result) {
373
+ is ValidationResult.Success -> null
374
+ is ValidationResult.Error -> {
375
+ "Invalid capture options:\n" + result.messages.joinToString("\n") { " - $it" }
376
+ }
377
+ }
378
+ }
379
+ }
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./plugin/build/index').default;
@@ -0,0 +1,5 @@
1
+ #import <FrameCaptureSpec/FrameCaptureSpec.h>
2
+
3
+ @interface FrameCapture : NSObject <NativeFrameCaptureSpec>
4
+
5
+ @end