react-native-capture-studio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +86 -0
  3. package/android/build.gradle +139 -0
  4. package/android/generated/java/com/capturestudio/NativeCaptureStudioSpec.java +48 -0
  5. package/android/generated/jni/CMakeLists.txt +31 -0
  6. package/android/generated/jni/RNCaptureStudioSpec-generated.cpp +44 -0
  7. package/android/generated/jni/RNCaptureStudioSpec.h +31 -0
  8. package/android/generated/jni/react/renderer/components/RNCaptureStudioSpec/RNCaptureStudioSpecJSI.h +56 -0
  9. package/android/gradle.properties +5 -0
  10. package/android/src/main/AndroidManifest.xml +13 -0
  11. package/android/src/main/AndroidManifestNew.xml +2 -0
  12. package/android/src/main/java/com/capturestudio/CaptureStudioModule.kt +177 -0
  13. package/android/src/main/java/com/capturestudio/CaptureStudioPackage.kt +33 -0
  14. package/android/src/main/java/com/capturestudio/data/CameraRepository.kt +43 -0
  15. package/android/src/main/java/com/capturestudio/data/processing/ImageProcessingWorker.kt +126 -0
  16. package/android/src/main/java/com/capturestudio/data/processing/ImageProcessor.kt +244 -0
  17. package/android/src/main/java/com/capturestudio/domain/CaptureOptions.kt +0 -0
  18. package/android/src/main/java/com/capturestudio/domain/model/ImageProcessingItem.kt +18 -0
  19. package/android/src/main/java/com/capturestudio/domain/model/ProcessingResult.kt +14 -0
  20. package/android/src/main/java/com/capturestudio/ui/camera/CameraActivity.kt +55 -0
  21. package/android/src/main/java/com/capturestudio/ui/camera/CameraUiState.kt +7 -0
  22. package/android/src/main/java/com/capturestudio/ui/camera/CameraViewModel.kt +34 -0
  23. package/android/src/main/java/com/capturestudio/ui/camera/CameraViewModelFactory.kt +17 -0
  24. package/android/src/main/res/layout/activity_camera.xml +10 -0
  25. package/ios/CaptureStudio.h +7 -0
  26. package/ios/CaptureStudio.mm +186 -0
  27. package/ios/ImageProcessor.h +22 -0
  28. package/ios/ImageProcessor.mm +383 -0
  29. package/ios/generated/Package.swift +59 -0
  30. package/ios/generated/ReactAppDependencyProvider/RCTAppDependencyProvider.h +25 -0
  31. package/ios/generated/ReactAppDependencyProvider/RCTAppDependencyProvider.mm +40 -0
  32. package/ios/generated/ReactAppDependencyProvider/ReactAppDependencyProvider.podspec +34 -0
  33. package/ios/generated/ReactCodegen/RCTModuleProviders.h +16 -0
  34. package/ios/generated/ReactCodegen/RCTModuleProviders.mm +51 -0
  35. package/ios/generated/ReactCodegen/RCTModulesConformingToProtocolsProvider.h +18 -0
  36. package/ios/generated/ReactCodegen/RCTModulesConformingToProtocolsProvider.mm +54 -0
  37. package/ios/generated/ReactCodegen/RCTThirdPartyComponentsProvider.h +16 -0
  38. package/ios/generated/ReactCodegen/RCTThirdPartyComponentsProvider.mm +30 -0
  39. package/ios/generated/ReactCodegen/RCTUnstableModulesRequiringMainQueueSetupProvider.h +14 -0
  40. package/ios/generated/ReactCodegen/RCTUnstableModulesRequiringMainQueueSetupProvider.mm +19 -0
  41. package/ios/generated/ReactCodegen/RNCaptureStudioSpec/RNCaptureStudioSpec-generated.mm +53 -0
  42. package/ios/generated/ReactCodegen/RNCaptureStudioSpec/RNCaptureStudioSpec.h +70 -0
  43. package/ios/generated/ReactCodegen/RNCaptureStudioSpecJSI.h +56 -0
  44. package/ios/generated/ReactCodegen/ReactCodegen.podspec +110 -0
  45. package/lib/commonjs/NativeCaptureStudio.js +9 -0
  46. package/lib/commonjs/NativeCaptureStudio.js.map +1 -0
  47. package/lib/commonjs/index.js +20 -0
  48. package/lib/commonjs/index.js.map +1 -0
  49. package/lib/module/NativeCaptureStudio.js +5 -0
  50. package/lib/module/NativeCaptureStudio.js.map +1 -0
  51. package/lib/module/index.js +13 -0
  52. package/lib/module/index.js.map +1 -0
  53. package/lib/typescript/commonjs/package.json +1 -0
  54. package/lib/typescript/commonjs/src/NativeCaptureStudio.d.ts +9 -0
  55. package/lib/typescript/commonjs/src/NativeCaptureStudio.d.ts.map +1 -0
  56. package/lib/typescript/commonjs/src/index.d.ts +19 -0
  57. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  58. package/lib/typescript/module/package.json +1 -0
  59. package/lib/typescript/module/src/NativeCaptureStudio.d.ts +9 -0
  60. package/lib/typescript/module/src/NativeCaptureStudio.d.ts.map +1 -0
  61. package/lib/typescript/module/src/index.d.ts +19 -0
  62. package/lib/typescript/module/src/index.d.ts.map +1 -0
  63. package/package.json +202 -0
  64. package/react-native-capture-studio.podspec +48 -0
  65. package/react-native.config.js +12 -0
  66. package/src/NativeCaptureStudio.ts +11 -0
  67. package/src/index.tsx +30 -0
@@ -0,0 +1,33 @@
1
+ package com.capturestudio
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class CaptureStudioPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == CaptureStudioModule.NAME) {
13
+ CaptureStudioModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
20
+ return ReactModuleInfoProvider {
21
+ val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
22
+ moduleInfos[CaptureStudioModule.NAME] = ReactModuleInfo(
23
+ CaptureStudioModule.NAME,
24
+ CaptureStudioModule.NAME,
25
+ false, // canOverrideExistingModule
26
+ false, // needsEagerInit
27
+ false, // isCxxModule
28
+ true // isTurboModule
29
+ )
30
+ moduleInfos
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,43 @@
1
+ package com.capturestudio.data
2
+
3
+ import android.content.Context
4
+ import androidx.camera.core.CameraSelector
5
+ import androidx.camera.core.Preview
6
+ import androidx.camera.lifecycle.ProcessCameraProvider
7
+ import androidx.camera.view.PreviewView
8
+ import androidx.core.content.ContextCompat
9
+ import androidx.lifecycle.LifecycleOwner
10
+
11
+ class CameraRepository {
12
+
13
+ private var cameraProvider: ProcessCameraProvider? = null
14
+
15
+ fun startCamera(
16
+ context: Context,
17
+ lifecycleOwner: LifecycleOwner,
18
+ previewView: PreviewView
19
+ ) {
20
+ val providerFuture = ProcessCameraProvider.getInstance(context)
21
+
22
+ providerFuture.addListener({
23
+ cameraProvider = providerFuture.get()
24
+
25
+ val preview = Preview.Builder().build()
26
+ preview.setSurfaceProvider(previewView.surfaceProvider)
27
+
28
+ val selector = CameraSelector.DEFAULT_BACK_CAMERA
29
+
30
+ cameraProvider?.unbindAll()
31
+ cameraProvider?.bindToLifecycle(
32
+ lifecycleOwner,
33
+ selector,
34
+ preview
35
+ )
36
+ }, ContextCompat.getMainExecutor(context))
37
+ }
38
+
39
+ fun release() {
40
+ cameraProvider?.unbindAll()
41
+ cameraProvider = null
42
+ }
43
+ }
@@ -0,0 +1,126 @@
1
+ package com.capturestudio.data.processing
2
+
3
+ import android.content.Context
4
+ import android.util.Log
5
+ import androidx.work.CoroutineWorker
6
+ import androidx.work.Data
7
+ import androidx.work.WorkerParameters
8
+ import kotlinx.coroutines.Dispatchers
9
+ import kotlinx.coroutines.withContext
10
+ import org.json.JSONArray
11
+ import org.json.JSONObject
12
+
13
+ /**
14
+ * WorkManager worker that processes images in the background.
15
+ * Handles multiple images sequentially with compression and watermarking.
16
+ */
17
+ class ImageProcessingWorker(
18
+ context: Context,
19
+ params: WorkerParameters
20
+ ) : CoroutineWorker(context, params) {
21
+
22
+ companion object {
23
+ private const val TAG = "ImageProcessingWorker"
24
+
25
+ // Input/Output data keys
26
+ const val KEY_IMAGES = "images"
27
+ const val KEY_RESULTS = "results"
28
+ }
29
+
30
+ override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
31
+ val imagesJson = inputData.getString(KEY_IMAGES)
32
+
33
+ if (imagesJson.isNullOrEmpty()) {
34
+ Log.e(TAG, "No images provided")
35
+ return@withContext Result.failure()
36
+ }
37
+
38
+ try {
39
+ val results = processImages(imagesJson)
40
+
41
+ val outputData = Data.Builder()
42
+ .putString(KEY_RESULTS, results.toString())
43
+ .build()
44
+
45
+ Log.d(TAG, "Processing completed successfully")
46
+ Result.success(outputData)
47
+ } catch (e: Exception) {
48
+ Log.e(TAG, "Processing failed", e)
49
+ Result.failure()
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Process all images from the JSON array.
55
+ *
56
+ * @param imagesJson JSON array string of image processing items
57
+ * @return JSONArray of processing results
58
+ */
59
+ private fun processImages(imagesJson: String): JSONArray {
60
+ val imageArray = JSONArray(imagesJson)
61
+ val results = JSONArray()
62
+
63
+ Log.d(TAG, "Processing ${imageArray.length()} images")
64
+
65
+ for (i in 0 until imageArray.length()) {
66
+ val imageObject = imageArray.getJSONObject(i)
67
+ val result = processSingleImage(imageObject)
68
+ results.put(result)
69
+ }
70
+
71
+ return results
72
+ }
73
+
74
+ /**
75
+ * Process a single image and return the result.
76
+ */
77
+ private fun processSingleImage(imageObject: JSONObject): JSONObject {
78
+ val originalPath = imageObject.optString("localPath", "")
79
+ val localPath = originalPath.replace("file://", "")
80
+ val timeStamp = imageObject.optString("timeStamp", "")
81
+ val isForOnlyWatermark = imageObject.optBoolean("isForOnlyWatermark", false)
82
+ val compressJpeg = imageObject.optBoolean("compressJpegImage", false)
83
+
84
+ // Validate input
85
+ if (localPath.isEmpty() || localPath == "undefined") {
86
+ Log.w(TAG, "Skipping invalid path: $localPath")
87
+ return createResultJson(originalPath, false, "Invalid path")
88
+ }
89
+
90
+ if (timeStamp.isEmpty() || timeStamp == "undefined") {
91
+ Log.w(TAG, "Skipping invalid timestamp for: $localPath")
92
+ return createResultJson(originalPath, false, "Invalid timestamp")
93
+ }
94
+
95
+ // Process the image
96
+ val result = ImageProcessor.processImage(
97
+ imagePath = localPath,
98
+ timeStamp = timeStamp,
99
+ isForOnlyWatermark = isForOnlyWatermark,
100
+ compressJpeg = compressJpeg
101
+ )
102
+
103
+ return if (result.isSuccess) {
104
+ createResultJson(originalPath, true, null)
105
+ } else {
106
+ val error = result.exceptionOrNull()?.message ?: "Unknown error"
107
+ Log.e(TAG, "Failed to process $localPath: $error")
108
+ createResultJson(originalPath, false, error)
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Create a JSON result object.
114
+ */
115
+ private fun createResultJson(
116
+ localPath: String,
117
+ success: Boolean,
118
+ error: String?
119
+ ): JSONObject {
120
+ return JSONObject().apply {
121
+ put("localPath", localPath)
122
+ put("success", success)
123
+ put("error", error ?: JSONObject.NULL)
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,244 @@
1
+ package com.capturestudio.data.processing
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.BitmapFactory
5
+ import android.graphics.Canvas
6
+ import android.graphics.Color
7
+ import android.graphics.Matrix
8
+ import android.graphics.Paint
9
+ import android.graphics.Rect
10
+ import android.util.Log
11
+ import androidx.exifinterface.media.ExifInterface
12
+ import java.io.ByteArrayOutputStream
13
+ import java.io.File
14
+ import java.io.FileOutputStream
15
+
16
+ /**
17
+ * Handles image processing operations including:
18
+ * - EXIF orientation correction
19
+ * - Watermark overlay
20
+ * - Compression to target file size (300-500KB)
21
+ */
22
+ object ImageProcessor {
23
+
24
+ private const val TAG = "ImageProcessor"
25
+
26
+ // Target file size range in KB
27
+ private const val MIN_SIZE_KB_DEFAULT = 300
28
+ private const val MIN_SIZE_KB_WATERMARK_ONLY = 400
29
+ private const val MAX_SIZE_KB = 500
30
+
31
+ /**
32
+ * Process a single image: rotate based on EXIF, add watermark, compress.
33
+ *
34
+ * @param imagePath Path to the image file
35
+ * @param timeStamp Watermark text to overlay
36
+ * @param isForOnlyWatermark Use higher minimum size if true
37
+ * @param compressJpeg Use JPEG format if true, WebP otherwise
38
+ * @return Result indicating success or failure with error message
39
+ */
40
+ fun processImage(
41
+ imagePath: String,
42
+ timeStamp: String,
43
+ isForOnlyWatermark: Boolean,
44
+ compressJpeg: Boolean
45
+ ): Result<Unit> {
46
+ return runCatching {
47
+ val file = File(imagePath)
48
+ if (!file.exists()) {
49
+ throw IllegalArgumentException("File not found: $imagePath")
50
+ }
51
+
52
+ Log.d(TAG, "Processing image: $imagePath")
53
+
54
+ // 1. Load bitmap
55
+ val originalBitmap = BitmapFactory.decodeFile(file.absolutePath)
56
+ ?: throw IllegalStateException("Failed to decode image: $imagePath")
57
+
58
+ // 2. Get EXIF orientation and rotate if needed
59
+ val exif = ExifInterface(file.absolutePath)
60
+ val orientation = exif.getAttributeInt(
61
+ ExifInterface.TAG_ORIENTATION,
62
+ ExifInterface.ORIENTATION_NORMAL
63
+ )
64
+ val rotatedBitmap = rotateBitmapIfNeeded(originalBitmap, orientation)
65
+
66
+ // Recycle original if we created a new bitmap
67
+ if (rotatedBitmap !== originalBitmap) {
68
+ originalBitmap.recycle()
69
+ }
70
+
71
+ // 3. Add watermark
72
+ val watermarkedBitmap = if (timeStamp.isNotEmpty()) {
73
+ addWatermark(rotatedBitmap, timeStamp).also {
74
+ if (it !== rotatedBitmap) rotatedBitmap.recycle()
75
+ }
76
+ } else {
77
+ rotatedBitmap
78
+ }
79
+
80
+ // 4. Compress to target size
81
+ val minSizeKB = if (isForOnlyWatermark) MIN_SIZE_KB_WATERMARK_ONLY else MIN_SIZE_KB_DEFAULT
82
+ compressToTargetSize(
83
+ bitmap = watermarkedBitmap,
84
+ outputPath = imagePath,
85
+ minKB = minSizeKB,
86
+ maxKB = MAX_SIZE_KB,
87
+ useJpeg = compressJpeg
88
+ )
89
+
90
+ watermarkedBitmap.recycle()
91
+
92
+ Log.d(TAG, "Successfully processed: $imagePath")
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Rotate bitmap based on EXIF orientation.
98
+ */
99
+ private fun rotateBitmapIfNeeded(bitmap: Bitmap, orientation: Int): Bitmap {
100
+ val rotationAngle = when (orientation) {
101
+ ExifInterface.ORIENTATION_ROTATE_90 -> 90f
102
+ ExifInterface.ORIENTATION_ROTATE_180 -> 180f
103
+ ExifInterface.ORIENTATION_ROTATE_270 -> 270f
104
+ else -> return bitmap // No rotation needed
105
+ }
106
+
107
+ val matrix = Matrix().apply {
108
+ postRotate(rotationAngle)
109
+ }
110
+
111
+ return Bitmap.createBitmap(
112
+ bitmap,
113
+ 0,
114
+ 0,
115
+ bitmap.width,
116
+ bitmap.height,
117
+ matrix,
118
+ true
119
+ )
120
+ }
121
+
122
+ /**
123
+ * Add a watermark to the bottom-right corner of the image.
124
+ * Yellow text on black background, matching iOS implementation.
125
+ */
126
+ private fun addWatermark(bitmap: Bitmap, text: String): Bitmap {
127
+ val mutableBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
128
+ val canvas = Canvas(mutableBitmap)
129
+
130
+ val imageWidth = mutableBitmap.width
131
+ val imageHeight = mutableBitmap.height
132
+
133
+ // Calculate sizes relative to image dimensions (matching iOS)
134
+ val textSize = imageWidth / 40f
135
+ val padding = imageWidth / 50f
136
+
137
+ // Text paint
138
+ val textPaint = Paint().apply {
139
+ color = Color.YELLOW
140
+ this.textSize = textSize
141
+ isAntiAlias = true
142
+ }
143
+
144
+ // Background paint
145
+ val bgPaint = Paint().apply {
146
+ color = Color.BLACK
147
+ style = Paint.Style.FILL
148
+ }
149
+
150
+ // Measure text
151
+ val textBounds = Rect()
152
+ textPaint.getTextBounds(text, 0, text.length, textBounds)
153
+
154
+ // Calculate background dimensions
155
+ val bgWidth = textBounds.width() + 2 * padding
156
+ val bgHeight = textBounds.height() + 2 * padding
157
+
158
+ // Position at bottom-right
159
+ val x = imageWidth - bgWidth - padding
160
+ val y = imageHeight - bgHeight - padding
161
+
162
+ // Draw black background rectangle
163
+ canvas.drawRect(x, y, x + bgWidth, y + bgHeight, bgPaint)
164
+
165
+ // Draw text
166
+ canvas.drawText(
167
+ text,
168
+ x + padding,
169
+ y + textBounds.height() + padding,
170
+ textPaint
171
+ )
172
+
173
+ return mutableBitmap
174
+ }
175
+
176
+ /**
177
+ * Compress bitmap to target file size using binary search.
178
+ * Tries to achieve file size between minKB and maxKB.
179
+ */
180
+ private fun compressToTargetSize(
181
+ bitmap: Bitmap,
182
+ outputPath: String,
183
+ minKB: Int,
184
+ maxKB: Int,
185
+ useJpeg: Boolean
186
+ ) {
187
+ val format = getCompressFormat(useJpeg)
188
+ Log.d(TAG, "Using compression format: $format (useJpeg=$useJpeg)")
189
+
190
+ var low = 0
191
+ var high = 100
192
+ var bestData: ByteArray? = null
193
+ var bestQuality = 100
194
+
195
+ // Binary search for optimal quality
196
+ while (low <= high) {
197
+ val quality = (low + high) / 2
198
+ val stream = ByteArrayOutputStream()
199
+
200
+ bitmap.compress(format, quality, stream)
201
+ val data = stream.toByteArray()
202
+ val sizeKB = data.size / 1024
203
+
204
+ bestData = data
205
+ bestQuality = quality
206
+
207
+ when {
208
+ sizeKB < minKB -> {
209
+ // Size too small, increase quality
210
+ low = quality + 1
211
+ }
212
+ sizeKB > maxKB -> {
213
+ // Size too large, decrease quality
214
+ high = quality - 1
215
+ }
216
+ else -> {
217
+ // Within range, done
218
+ Log.d(TAG, "Found optimal quality: $quality, size: ${sizeKB}KB")
219
+ break
220
+ }
221
+ }
222
+ }
223
+
224
+ // Write to file
225
+ bestData?.let { data ->
226
+ FileOutputStream(outputPath).use { fos ->
227
+ fos.write(data)
228
+ }
229
+ Log.d(TAG, "Wrote ${data.size / 1024}KB at quality $bestQuality to $outputPath")
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Get the appropriate compression format based on preference.
235
+ */
236
+ private fun getCompressFormat(useJpeg: Boolean): Bitmap.CompressFormat {
237
+ return if (useJpeg) {
238
+ Bitmap.CompressFormat.JPEG
239
+ } else {
240
+ @Suppress("DEPRECATION")
241
+ Bitmap.CompressFormat.WEBP
242
+ }
243
+ }
244
+ }
@@ -0,0 +1,18 @@
1
+ package com.capturestudio.domain.model
2
+
3
+ /**
4
+ * Represents an image to be processed with compression and watermarking.
5
+ *
6
+ * @property localPath The file path to the image (may include "file://" prefix)
7
+ * @property timeStamp The timestamp text to overlay as watermark
8
+ * @property isForOnlyWatermark If true, uses higher minimum size (400KB vs 300KB)
9
+ * @property compressJpegImage If true, uses JPEG format; otherwise uses WebP
10
+ * @property replaceOriginal If true, overwrites the original file
11
+ */
12
+ data class ImageProcessingItem(
13
+ val localPath: String,
14
+ val timeStamp: String,
15
+ val isForOnlyWatermark: Boolean = false,
16
+ val compressJpegImage: Boolean = false,
17
+ val replaceOriginal: Boolean = true
18
+ )
@@ -0,0 +1,14 @@
1
+ package com.capturestudio.domain.model
2
+
3
+ /**
4
+ * Represents the result of processing a single image.
5
+ *
6
+ * @property localPath The original path of the processed image
7
+ * @property success Whether processing completed successfully
8
+ * @property error Error message if processing failed, null otherwise
9
+ */
10
+ data class ProcessingResult(
11
+ val localPath: String,
12
+ val success: Boolean,
13
+ val error: String? = null
14
+ )
@@ -0,0 +1,55 @@
1
+ package com.capturestudio.ui.camera
2
+
3
+ import android.Manifest
4
+ import android.content.pm.PackageManager
5
+ import android.os.Bundle
6
+ import androidx.activity.result.contract.ActivityResultContracts
7
+ import androidx.activity.viewModels
8
+ import androidx.appcompat.app.AppCompatActivity
9
+ import androidx.camera.view.PreviewView
10
+ import androidx.core.content.ContextCompat
11
+ import com.capturestudio.R
12
+
13
+ class CameraActivity : AppCompatActivity() {
14
+
15
+ private val viewModel: CameraViewModel by viewModels {
16
+ CameraViewModelFactory()
17
+ }
18
+
19
+ private lateinit var previewView: PreviewView
20
+
21
+ private val permissionLauncher =
22
+ registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
23
+ if (granted) {
24
+ startCamera()
25
+ }
26
+ }
27
+
28
+ override fun onCreate(savedInstanceState: Bundle?) {
29
+ super.onCreate(savedInstanceState)
30
+ setContentView(R.layout.activity_camera)
31
+
32
+ previewView = findViewById(R.id.previewView)
33
+
34
+ if (hasCameraPermission()) {
35
+ startCamera()
36
+ } else {
37
+ permissionLauncher.launch(Manifest.permission.CAMERA)
38
+ }
39
+ }
40
+
41
+ private fun startCamera() {
42
+ viewModel.startCamera(
43
+ context = this,
44
+ lifecycleOwner = this,
45
+ previewView = previewView
46
+ )
47
+ }
48
+
49
+ private fun hasCameraPermission(): Boolean {
50
+ return ContextCompat.checkSelfPermission(
51
+ this,
52
+ Manifest.permission.CAMERA
53
+ ) == PackageManager.PERMISSION_GRANTED
54
+ }
55
+ }
@@ -0,0 +1,7 @@
1
+ package com.capturestudio.ui.camera
2
+
3
+ data class CameraUiState(
4
+ val isCameraReady: Boolean = false,
5
+ val isCapturing: Boolean = false,
6
+ val error: String? = null
7
+ )
@@ -0,0 +1,34 @@
1
+ package com.capturestudio.ui.camera
2
+
3
+ import android.content.Context
4
+ import androidx.camera.view.PreviewView
5
+ import androidx.lifecycle.LifecycleOwner
6
+ import androidx.lifecycle.LiveData
7
+ import androidx.lifecycle.MutableLiveData
8
+ import androidx.lifecycle.ViewModel
9
+ import com.capturestudio.data.CameraRepository
10
+
11
+ class CameraViewModel(
12
+ private val cameraRepository: CameraRepository
13
+ ) : ViewModel() {
14
+
15
+ private val _uiState = MutableLiveData(CameraUiState())
16
+ val uiState: LiveData<CameraUiState> = _uiState
17
+
18
+ fun startCamera(
19
+ context: Context,
20
+ lifecycleOwner: LifecycleOwner,
21
+ previewView: PreviewView
22
+ ) {
23
+ cameraRepository.startCamera(
24
+ context = context,
25
+ lifecycleOwner = lifecycleOwner,
26
+ previewView = previewView
27
+ )
28
+ }
29
+
30
+ override fun onCleared() {
31
+ super.onCleared()
32
+ cameraRepository.release()
33
+ }
34
+ }
@@ -0,0 +1,17 @@
1
+ package com.capturestudio.ui.camera
2
+
3
+ import androidx.lifecycle.ViewModel
4
+ import androidx.lifecycle.ViewModelProvider
5
+ import com.capturestudio.data.CameraRepository
6
+
7
+ class CameraViewModelFactory : ViewModelProvider.Factory {
8
+
9
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
10
+ if (modelClass.isAssignableFrom(CameraViewModel::class.java)) {
11
+ return CameraViewModel(
12
+ cameraRepository = CameraRepository()
13
+ ) as T
14
+ }
15
+ throw IllegalArgumentException("Unknown ViewModel class")
16
+ }
17
+ }
@@ -0,0 +1,10 @@
1
+ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
2
+ android:layout_width="match_parent"
3
+ android:layout_height="match_parent"
4
+ android:background="#000">
5
+
6
+ <androidx.camera.view.PreviewView
7
+ android:id="@+id/previewView"
8
+ android:layout_width="match_parent"
9
+ android:layout_height="match_parent" />
10
+ </FrameLayout>
@@ -0,0 +1,7 @@
1
+ #ifdef __cplusplus
2
+ #import "generated/ReactCodegen/RNCaptureStudioSpec/RNCaptureStudioSpec.h"
3
+ #endif
4
+
5
+ @interface CaptureStudio : NSObject <NativeCaptureStudioSpec>
6
+
7
+ @end