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.
- package/FrameCapture.podspec +21 -0
- package/LICENSE +20 -0
- package/README.md +158 -0
- package/android/build.gradle +77 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/com/framecapture/CaptureManager.kt +831 -0
- package/android/src/main/java/com/framecapture/Constants.kt +196 -0
- package/android/src/main/java/com/framecapture/ErrorHandler.kt +165 -0
- package/android/src/main/java/com/framecapture/FrameCaptureModule.kt +653 -0
- package/android/src/main/java/com/framecapture/FrameCapturePackage.kt +32 -0
- package/android/src/main/java/com/framecapture/OverlayRenderer.kt +423 -0
- package/android/src/main/java/com/framecapture/PermissionHandler.kt +150 -0
- package/android/src/main/java/com/framecapture/ScreenCaptureService.kt +366 -0
- package/android/src/main/java/com/framecapture/StorageManager.kt +221 -0
- package/android/src/main/java/com/framecapture/capture/BitmapProcessor.kt +157 -0
- package/android/src/main/java/com/framecapture/capture/CaptureEventEmitter.kt +120 -0
- package/android/src/main/java/com/framecapture/models/CaptureModels.kt +302 -0
- package/android/src/main/java/com/framecapture/models/EnumsAndExtensions.kt +60 -0
- package/android/src/main/java/com/framecapture/models/OverlayModels.kt +154 -0
- package/android/src/main/java/com/framecapture/service/CaptureNotificationManager.kt +286 -0
- package/android/src/main/java/com/framecapture/storage/StorageStrategies.kt +317 -0
- package/android/src/main/java/com/framecapture/utils/ValidationUtils.kt +379 -0
- package/app.plugin.js +1 -0
- package/ios/FrameCapture.h +5 -0
- package/ios/FrameCapture.mm +21 -0
- package/lib/module/NativeFrameCapture.js +24 -0
- package/lib/module/NativeFrameCapture.js.map +1 -0
- package/lib/module/api.js +146 -0
- package/lib/module/api.js.map +1 -0
- package/lib/module/constants.js +67 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/errors.js +19 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/events.js +58 -0
- package/lib/module/events.js.map +1 -0
- package/lib/module/index.js +24 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/normalize.js +51 -0
- package/lib/module/normalize.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +165 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/validation.js +190 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/plugin/src/index.d.ts +4 -0
- package/lib/typescript/plugin/src/index.d.ts.map +1 -0
- package/lib/typescript/src/NativeFrameCapture.d.ts +75 -0
- package/lib/typescript/src/NativeFrameCapture.d.ts.map +1 -0
- package/lib/typescript/src/api.d.ts +66 -0
- package/lib/typescript/src/api.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +41 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +14 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/events.d.ts +30 -0
- package/lib/typescript/src/events.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +12 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/normalize.d.ts +43 -0
- package/lib/typescript/src/normalize.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +247 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/validation.d.ts +15 -0
- package/lib/typescript/src/validation.d.ts.map +1 -0
- package/package.json +196 -0
- package/plugin/build/index.js +48 -0
- package/src/NativeFrameCapture.ts +86 -0
- package/src/api.ts +189 -0
- package/src/constants.ts +69 -0
- package/src/errors.ts +21 -0
- package/src/events.ts +61 -0
- package/src/index.tsx +31 -0
- package/src/normalize.ts +81 -0
- package/src/types.ts +327 -0
- 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;
|