react-native-rn-story-editor 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,30 +1,125 @@
1
1
  # react-native-rn-story-editor
2
2
 
3
- This is library for the Editing tools
3
+ [![npm version](https://img.shields.io/npm/v/react-native-rn-story-editor.svg)](https://www.npmjs.com/package/react-native-rn-story-editor)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Platform - Android](https://img.shields.io/badge/platform-Android-green.svg)](https://www.android.com)
6
+ [![Platform - iOS](https://img.shields.io/badge/platform-iOS-blue.svg)](https://www.apple.com)
7
+
8
+ A powerful React Native story editor with layer management, drag-and-drop, text/image overlays, and export functionality. Built with React Native Fabric architecture for optimal performance.
9
+
10
+ ## Features
11
+
12
+ - **Layer Management** - Add, edit, and delete text and image layers
13
+ - **Drag & Drop** - Move layers around with touch gestures
14
+ - **Base Image Support** - Set background images from base64
15
+ - **Text Layers** - Add styled text with shadows and positioning
16
+ - **Image Overlays** - Add overlay images with automatic scaling
17
+ - **Touch Interactions** - Tap to edit/delete, drag to move
18
+ - **Export Options** - Save to gallery or emit base64 to JavaScript
19
+ - **Fabric Architecture** - Built with React Native's new rendering system
20
+ - **Cross-Platform** - Works on both Android and iOS
21
+ - **TypeScript Support** - Full type safety and IntelliSense
4
22
 
5
23
  ## Installation
6
24
 
7
- ```sh
25
+ ```bash
8
26
  npm install react-native-rn-story-editor
9
27
  ```
10
28
 
11
29
  ## Usage
12
30
 
13
- ```js
14
- import { StoryEditorView, StoryEditorCommands } from "react-native-rn-story-editor";
31
+ ```jsx
32
+ import { useRef, useEffect } from 'react';
33
+ import { View, StyleSheet, DeviceEventEmitter } from 'react-native';
34
+ import { StoryEditorView, StoryEditorCommands } from 'react-native-rn-story-editor';
35
+
36
+ export default function App() {
37
+ const editorRef = useRef(null);
38
+
39
+ useEffect(() => {
40
+ // Listen for export events
41
+ const subscription = DeviceEventEmitter.addListener('onExportImage', (event) => {
42
+ console.log('Exported image base64:', event.image);
43
+ });
44
+ return () => subscription.remove();
45
+ }, []);
46
+
47
+ return (
48
+ <View style={styles.container}>
49
+ <StoryEditorView
50
+ ref={editorRef}
51
+ colorString="#32a852"
52
+ baseImage="data:image/png;base64,iVBORw0KGgo..."
53
+ style={styles.editor}
54
+ />
55
+
56
+ {/* Export to gallery or get base64 */}
57
+ <Button
58
+ title="Export Image"
59
+ onPress={() => StoryEditorCommands.exportImage(editorRef.current)}
60
+ />
61
+
62
+ {/* Show text input dialog */}
63
+ <Button
64
+ title="Add Text"
65
+ onPress={() => StoryEditorCommands.showTextInput(editorRef.current)}
66
+ />
67
+ </View>
68
+ );
69
+ }
70
+
71
+ const styles = StyleSheet.create({
72
+ container: { flex: 1 },
73
+ editor: { width: 300, height: 400 }
74
+ });
75
+ ```
76
+
77
+ ## Props
78
+
79
+ | Prop | Type | Default | Description |
80
+ |------|------|----------|-------------|
81
+ | `color` | `number` | `Color.TRANSPARENT` | Numeric color value (e.g., `0xFF32a852`) |
82
+ | `colorString` | `string` | `undefined` | Hex color string (e.g., `"#32a852"`) |
83
+ | `baseImage` | `string` | `undefined` | Base64 encoded background image (supports data URI format) |
84
+ | `addText` | `string` | `undefined` | Add text layer programmatically |
85
+ | `addImage` | `string` | `undefined` | Add overlay image from base64 (supports data URI format) |
86
+ | `style` | `ViewStyle` | `undefined` | React Native style object |
87
+
88
+ ## Commands
89
+
90
+ | Command | Parameters | Description |
91
+ |---------|------------|-------------|
92
+ | `exportImage` | `(ref: React.Ref)` | Shows export dialog with gallery save and database options |
93
+ | `showTextInput` | `(ref: React.Ref)` | Shows native text input dialog to add text layer |
15
94
 
16
- // Use the component with numeric color
17
- <StoryEditorView color={0xFF32a852} style={styles.box} />
95
+ ## Events
18
96
 
19
- // Or use the component with hex string color
20
- <StoryEditorView colorString="#32a852" style={styles.box} />
97
+ ### onExportImage
21
98
 
22
- // Use commands when needed
23
- const ref = useRef(null);
24
- StoryEditorCommands.exportImage(ref);
25
- StoryEditorCommands.showTextInput(ref);
99
+ Emitted when user selects "Send to Database" in the export dialog.
100
+
101
+ ```javascript
102
+ DeviceEventEmitter.addListener('onExportImage', (event) => {
103
+ // event.image contains the base64 string
104
+ console.log(event.image);
105
+ });
26
106
  ```
27
107
 
108
+ ## Touch Interactions
109
+
110
+ - **Drag**: Touch and hold to drag text/image layers around
111
+ - **Tap Text**: Quick tap to open edit/delete dialog
112
+ - **Tap Image**: Quick tap to open delete options
113
+ - **Stacking**: Layers stack in order added, last added is on top
114
+
115
+ ## Export Options
116
+
117
+ When `exportImage` is called, users see a dialog with:
118
+
119
+ - **Download to Gallery**: Saves the composed image to device photos
120
+ - **Send to Database**: Emits `onExportImage` event with base64 data
121
+ - **Cancel**: Dismiss the dialog
122
+
28
123
  ## Testing
29
124
 
30
125
  ### Unit Tests
@@ -47,20 +142,29 @@ npm install react-native@0.73
47
142
  ```
48
143
 
49
144
  ### Cross-Platform Verification
50
- - Android: Tested on API 24+ (emulators/devices)
51
- - iOS: Test on iOS 12+ (simulators/devices)
52
- - React Native: Test with multiple versions
53
- - TypeScript: All types compile correctly
54
- - Props: Color, style, and other props work
55
- - Commands: exportImage and showTextInput functions
56
-
57
- ### Before Publishing Checklist
58
- - [ ] All tests pass
59
- - [ ] Manual testing on Android/iOS
60
- - [ ] TypeScript builds without errors
61
- - [ ] Example app runs successfully
62
- - [ ] Documentation is up to date
63
- - [ ] Version is incremented
145
+ - Android: Tested on API 24+ (emulators/devices)
146
+ - iOS: Test on iOS 12+ (simulators/devices)
147
+ - React Native: Test with multiple versions
148
+ - TypeScript: All types compile correctly
149
+ - Props: Color, style, baseImage, addText, addImage
150
+ - Commands: exportImage and showTextInput functions
151
+ - Events: onExportImage event emission
152
+
153
+ ## Version History
154
+
155
+ ### 0.2.0
156
+ - Added layer management (text and image layers)
157
+ - Added drag-and-drop functionality
158
+ - Added base64 image support for backgrounds and overlays
159
+ - Added export functionality (gallery save + base64 emission)
160
+ - Added touch interactions (tap to edit, drag to move)
161
+ - Fixed base64 data URI parsing
162
+ - Improved event system for JavaScript communication
163
+
164
+ ### 0.1.0
165
+ - Initial release
166
+ - Basic view component with color support
167
+ - Native commands structure
64
168
 
65
169
  ## Contributing
66
170
 
@@ -0,0 +1,32 @@
1
+ package com.rnstoryeditor
2
+
3
+ import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
5
+ import com.facebook.react.bridge.ReactMethod
6
+ import com.facebook.react.modules.core.DeviceEventManagerModule
7
+ import com.facebook.react.bridge.Arguments
8
+ import com.facebook.react.bridge.WritableMap
9
+
10
+ class RnStoryEditorEventEmitter(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
11
+
12
+ override fun getName(): String {
13
+ return "RnStoryEditorEventEmitter"
14
+ }
15
+
16
+ companion object {
17
+ private var reactAppContext: ReactApplicationContext? = null
18
+
19
+ fun setReactContext(context: ReactApplicationContext) {
20
+ reactAppContext = context
21
+ }
22
+
23
+ fun emitEvent(eventName: String, base64: String) {
24
+ val params = Arguments.createMap()
25
+ params.putString("image", base64)
26
+ reactAppContext?.let { ctx ->
27
+ ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
28
+ .emit(eventName, params)
29
+ }
30
+ }
31
+ }
32
+ }
@@ -8,9 +8,15 @@ import com.facebook.react.uimanager.ViewManager
8
8
 
9
9
  class RnStoryEditorViewPackage : BaseReactPackage() {
10
10
  override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
11
+ RnStoryEditorEventEmitter.setReactContext(reactContext)
11
12
  return listOf(RnStoryEditorViewManager())
12
13
  }
13
14
 
15
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
16
+ RnStoryEditorEventEmitter.setReactContext(reactContext)
17
+ return listOf(RnStoryEditorEventEmitter(reactContext))
18
+ }
19
+
14
20
  override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
15
21
 
16
22
  override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { emptyMap() }
@@ -1,15 +1,383 @@
1
1
  package com.rnstoryeditor
2
2
 
3
3
  import android.content.Context
4
+ import android.graphics.*
4
5
  import android.util.AttributeSet
6
+ import android.view.MotionEvent
5
7
  import android.view.View
8
+ import android.app.AlertDialog
9
+ import android.widget.EditText
10
+ import android.widget.ImageView
11
+ import android.util.Log
12
+ import android.widget.ScrollView
13
+ import android.widget.TextView
14
+ import android.os.Environment
15
+ import android.content.ContentValues
16
+ import android.provider.MediaStore
17
+ import android.net.Uri
18
+ import android.os.Build
19
+ import androidx.core.content.FileProvider
20
+ import com.facebook.react.bridge.Arguments
21
+ import com.facebook.react.bridge.ReactContext
22
+ import com.facebook.react.modules.core.DeviceEventManagerModule
23
+ import android.util.Base64
24
+ import java.io.ByteArrayOutputStream
25
+ import java.io.File
26
+ import java.io.FileOutputStream
27
+ import java.io.OutputStream
28
+ import java.text.SimpleDateFormat
29
+ import java.util.Date
30
+ import java.util.Locale
6
31
 
7
- class RnStoryEditorView : View {
8
- constructor(context: Context?) : super(context)
9
- constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
10
- constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
11
- context,
12
- attrs,
13
- defStyleAttr
14
- )
32
+ sealed class Layer {
33
+ data class TextLayer(var text: String, var x: Float, var y: Float, var paint: Paint) : Layer()
34
+ data class ImageLayer(var bitmap: Bitmap, var x: Float, var y: Float, var width: Float, var height: Float) : Layer()
35
+ }
36
+
37
+ class RnStoryEditorView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
38
+
39
+ private var baseBitmap: Bitmap? = null
40
+ private val layers = mutableListOf<Layer>()
41
+ private var selectedLayer: Layer? = null
42
+ private var lastTouchX = 0f
43
+ private var lastTouchY = 0f
44
+ private var touchStartTime = 0L
45
+ private var isDragging = false
46
+
47
+ fun setBaseImage(bitmap: Bitmap) {
48
+ baseBitmap = bitmap
49
+ invalidate()
50
+ }
51
+
52
+ fun addText(text: String) {
53
+ val paint = Paint()
54
+ paint.color = Color.WHITE
55
+ paint.textSize = 48f
56
+ paint.isAntiAlias = true
57
+ paint.setShadowLayer(8f, 2f, 2f, Color.BLACK)
58
+ paint.style = Paint.Style.FILL
59
+ paint.textAlign = Paint.Align.CENTER
60
+
61
+ // Position text in center of canvas
62
+ val x = width / 2f
63
+ val y = height / 2f + (layers.size * 60f) // Stack texts vertically from center
64
+
65
+ layers.add(Layer.TextLayer(text, x, y, paint))
66
+ invalidate()
67
+ }
68
+
69
+ fun addImage(base64: String) {
70
+ base64?.let {
71
+ val bytes = Base64.decode(it, Base64.DEFAULT)
72
+ val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
73
+
74
+ // Scale image to reasonable size (max 200dp width)
75
+ val maxWidth = 200f * resources.displayMetrics.density
76
+ val scale = maxWidth / bitmap.width
77
+ val scaledWidth = bitmap.width * scale
78
+ val scaledHeight = bitmap.height * scale
79
+
80
+ val scaledBitmap = Bitmap.createScaledBitmap(bitmap, scaledWidth.toInt(), scaledHeight.toInt(), true)
81
+
82
+ // Position image in center
83
+ val x = (width - scaledWidth) / 2f
84
+ val y = (height - scaledHeight) / 2f
85
+
86
+ layers.add(Layer.ImageLayer(scaledBitmap, x, y, scaledWidth, scaledHeight))
87
+ invalidate()
88
+ }
89
+ }
90
+
91
+ fun showTextInputDialog() {
92
+ Log.d("RnStoryEditorView", "showTextInputDialog called")
93
+ val editText = EditText(context)
94
+ editText.hint = "Enter text here"
95
+
96
+ AlertDialog.Builder(context)
97
+ .setTitle("Add Text Layer")
98
+ .setView(editText)
99
+ .setPositiveButton("Add") { dialog, _ ->
100
+ val inputText = editText.text.toString()
101
+ Log.d("RnStoryEditorView", "Input text: $inputText")
102
+ if (inputText.isNotEmpty()) {
103
+ addText(inputText)
104
+ }
105
+ dialog.dismiss()
106
+ }
107
+ .setNegativeButton("Cancel") { dialog, _ ->
108
+ dialog.dismiss()
109
+ }
110
+ .show()
111
+ }
112
+
113
+ fun showImageOptionsDialog(imageLayer: Layer.ImageLayer) {
114
+ Log.d("RnStoryEditorView", "showImageOptionsDialog called")
115
+
116
+ AlertDialog.Builder(context)
117
+ .setTitle("Image Options")
118
+ .setItems(arrayOf("Delete Image")) { dialog, which ->
119
+ when (which) {
120
+ 0 -> {
121
+ layers.remove(imageLayer)
122
+ invalidate()
123
+ }
124
+ }
125
+ dialog.dismiss()
126
+ }
127
+ .setNegativeButton("Cancel") { dialog, _ ->
128
+ dialog.dismiss()
129
+ }
130
+ .show()
131
+ }
132
+
133
+ fun showEditTextDialog(textLayer: Layer.TextLayer) {
134
+ Log.d("RnStoryEditorView", "showEditTextDialog called for: ${textLayer.text}")
135
+ val editText = EditText(context)
136
+ editText.setText(textLayer.text)
137
+ editText.hint = "Enter text here"
138
+
139
+ AlertDialog.Builder(context)
140
+ .setTitle("Edit Text Layer")
141
+ .setView(editText)
142
+ .setPositiveButton("Update") { dialog, _ ->
143
+ val inputText = editText.text.toString()
144
+ Log.d("RnStoryEditorView", "Updated text: $inputText")
145
+ if (inputText.isNotEmpty()) {
146
+ textLayer.text = inputText
147
+ invalidate()
148
+ }
149
+ dialog.dismiss()
150
+ }
151
+ .setNegativeButton("Cancel") { dialog, _ ->
152
+ dialog.dismiss()
153
+ }
154
+ .setNeutralButton("Delete") { dialog, _ ->
155
+ layers.remove(textLayer)
156
+ invalidate()
157
+ dialog.dismiss()
158
+ }
159
+ .show()
160
+ }
161
+
162
+ override fun onDraw(canvas: Canvas) {
163
+ super.onDraw(canvas)
164
+ baseBitmap?.let {
165
+ canvas.drawBitmap(it, null, Rect(0, 0, width, height), null)
166
+ }
167
+
168
+ layers.forEach { layer ->
169
+ when (layer) {
170
+ is Layer.TextLayer -> {
171
+ canvas.drawText(layer.text, layer.x, layer.y, layer.paint)
172
+ }
173
+ is Layer.ImageLayer -> {
174
+ canvas.drawBitmap(layer.bitmap, layer.x, layer.y, null)
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ override fun onTouchEvent(event: MotionEvent): Boolean {
181
+ when (event.action) {
182
+ MotionEvent.ACTION_DOWN -> {
183
+ touchStartTime = System.currentTimeMillis()
184
+ lastTouchX = event.x
185
+ lastTouchY = event.y
186
+ isDragging = false
187
+
188
+ // Check if touch is on a layer (text or image)
189
+ selectedLayer = null
190
+ for (layer in layers.reversed()) { // Check from top to bottom
191
+ when (layer) {
192
+ is Layer.TextLayer -> {
193
+ val textBounds = Rect()
194
+ layer.paint.getTextBounds(layer.text, 0, layer.text.length, textBounds)
195
+
196
+ // Calculate text bounds with center alignment
197
+ val textLeft = layer.x - textBounds.width() / 2
198
+ val textRight = layer.x + textBounds.width() / 2
199
+ val textTop = layer.y - textBounds.height()
200
+ val textBottom = layer.y
201
+
202
+ if (event.x >= textLeft && event.x <= textRight &&
203
+ event.y >= textTop && event.y <= textBottom) {
204
+ selectedLayer = layer
205
+ return true
206
+ }
207
+ }
208
+ is Layer.ImageLayer -> {
209
+ // Check if touch is within image bounds
210
+ if (event.x >= layer.x && event.x <= layer.x + layer.width &&
211
+ event.y >= layer.y && event.y <= layer.y + layer.height) {
212
+ selectedLayer = layer
213
+ return true
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ MotionEvent.ACTION_MOVE -> {
220
+ if (!isDragging && selectedLayer != null) {
221
+ val touchDuration = System.currentTimeMillis() - touchStartTime
222
+ if (touchDuration > 100) { // Start dragging after 100ms
223
+ isDragging = true
224
+ }
225
+ }
226
+
227
+ if (isDragging && selectedLayer != null) {
228
+ val dx = event.x - lastTouchX
229
+ val dy = event.y - lastTouchY
230
+
231
+ val currentLayer = selectedLayer
232
+ when (currentLayer) {
233
+ is Layer.TextLayer -> {
234
+ currentLayer.x += dx
235
+ currentLayer.y += dy
236
+ }
237
+ is Layer.ImageLayer -> {
238
+ currentLayer.x += dx
239
+ currentLayer.y += dy
240
+ }
241
+ null -> { /* Handle null case */ }
242
+ }
243
+ invalidate()
244
+ lastTouchX = event.x
245
+ lastTouchY = event.y
246
+ }
247
+ }
248
+ MotionEvent.ACTION_UP -> {
249
+ val touchDuration = System.currentTimeMillis() - touchStartTime
250
+ val currentLayer = selectedLayer
251
+ if (!isDragging && currentLayer is Layer.TextLayer && touchDuration < 500) {
252
+ // Quick tap on text - show edit dialog
253
+ showEditTextDialog(currentLayer)
254
+ } else if (!isDragging && currentLayer is Layer.ImageLayer && touchDuration < 500) {
255
+ // Quick tap on image - show options dialog
256
+ showImageOptionsDialog(currentLayer)
257
+ }
258
+ isDragging = false
259
+ }
260
+ }
261
+ return true
262
+ }
263
+
264
+ fun saveImageToGallery(bitmap: Bitmap): Boolean {
265
+ return try {
266
+ val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
267
+ val filename = "StoryEditor_$timestamp.png"
268
+
269
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
270
+ // Android 10+ use MediaStore
271
+ val resolver = context.contentResolver
272
+ val contentValues = ContentValues().apply {
273
+ put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
274
+ put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
275
+ put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/StoryEditor")
276
+ }
277
+
278
+ val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
279
+ uri?.let {
280
+ val outputStream: OutputStream? = resolver.openOutputStream(it)
281
+ outputStream?.use { stream ->
282
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
283
+ }
284
+ }
285
+ } else {
286
+ // Android 9 and below use File
287
+ val directory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "StoryEditor")
288
+ if (!directory.exists()) {
289
+ directory.mkdirs()
290
+ }
291
+
292
+ val imageFile = File(directory, filename)
293
+ val outputStream = FileOutputStream(imageFile)
294
+ outputStream.use { stream ->
295
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
296
+ }
297
+
298
+ // Add to gallery
299
+ val contentValues = ContentValues().apply {
300
+ put(MediaStore.Images.Media.DATA, imageFile.absolutePath)
301
+ put(MediaStore.Images.Media.MIME_TYPE, "image/png")
302
+ }
303
+ context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
304
+ }
305
+
306
+ Log.d("RnStoryEditorView", "Image saved to gallery: $filename")
307
+ true
308
+ } catch (e: Exception) {
309
+ Log.e("RnStoryEditorView", "Error saving image: ${e.message}")
310
+ false
311
+ }
312
+ }
313
+
314
+ // Merge layers and send base64 back to JS
315
+ fun exportAndSendBase64() {
316
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
317
+ val canvas = Canvas(bitmap)
318
+ draw(canvas)
319
+
320
+ val outputStream = ByteArrayOutputStream()
321
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
322
+ val byteArray = outputStream.toByteArray()
323
+ val base64 = Base64.encodeToString(byteArray, Base64.NO_WRAP)
324
+
325
+ // Show export dialog with download and database options
326
+ showExportPreviewDialog(bitmap, base64)
327
+ }
328
+
329
+ fun showExportPreviewDialog(bitmap: Bitmap, base64: String) {
330
+ val imageView = ImageView(context)
331
+ imageView.setImageBitmap(bitmap)
332
+ imageView.setPadding(20, 20, 20, 20)
333
+
334
+ val infoText = TextView(context)
335
+ val sizeKB = base64.length / 1024
336
+ val sizeMB = sizeKB / 1024.0
337
+ val imageCount = layers.count { it is Layer.ImageLayer }
338
+ val textCount = layers.count { it is Layer.TextLayer }
339
+ infoText.text = """
340
+ Export Information:
341
+ • Image Size: ${bitmap.width} x ${bitmap.height}px
342
+ • File Size: ${sizeKB} KB (${String.format("%.2f", sizeMB)} MB)
343
+ • Base64 Length: ${base64.length} characters
344
+ • Format: PNG with Base64 encoding
345
+ • Image Layers: $imageCount
346
+ • Text Layers: $textCount
347
+ • Total Layers: ${layers.size}
348
+
349
+ Choose export option below.
350
+ """.trimIndent()
351
+ infoText.setPadding(20, 10, 20, 20)
352
+ infoText.textSize = 14f
353
+
354
+ val scrollView = ScrollView(context)
355
+ val layout = android.widget.LinearLayout(context)
356
+ layout.orientation = android.widget.LinearLayout.VERTICAL
357
+ layout.addView(imageView)
358
+ layout.addView(infoText)
359
+ scrollView.addView(layout)
360
+
361
+ AlertDialog.Builder(context)
362
+ .setTitle("Export Options")
363
+ .setView(scrollView)
364
+ .setPositiveButton("Download to Gallery") { dialog, _ ->
365
+ val success = saveImageToGallery(bitmap)
366
+ if (success) {
367
+ android.widget.Toast.makeText(context, "Image saved to gallery!", android.widget.Toast.LENGTH_SHORT).show()
368
+ } else {
369
+ android.widget.Toast.makeText(context, "Failed to save image", android.widget.Toast.LENGTH_SHORT).show()
370
+ }
371
+ dialog.dismiss()
372
+ }
373
+ .setNeutralButton("Send to Database") { dialog, _ ->
374
+ // Send event to JS with base64
375
+ RnStoryEditorEventEmitter.emitEvent("onExportImage", base64)
376
+ dialog.dismiss()
377
+ }
378
+ .setNegativeButton("Cancel") { dialog, _ ->
379
+ dialog.dismiss()
380
+ }
381
+ .show()
382
+ }
15
383
  }
@@ -8,7 +8,7 @@ import com.facebook.react.uimanager.ViewManagerDelegate
8
8
  import com.facebook.react.uimanager.annotations.ReactProp
9
9
  import com.facebook.react.viewmanagers.RnStoryEditorViewManagerInterface
10
10
  import com.facebook.react.viewmanagers.RnStoryEditorViewManagerDelegate
11
- import com.facebook.react.bridge.ReadableMap
11
+ import com.facebook.react.bridge.ReadableArray
12
12
 
13
13
  @ReactModule(name = RnStoryEditorViewManager.NAME)
14
14
  class RnStoryEditorViewManager : SimpleViewManager<RnStoryEditorView>(),
@@ -51,6 +51,48 @@ class RnStoryEditorViewManager : SimpleViewManager<RnStoryEditorView>(),
51
51
  view?.setBackgroundColor(androidColor)
52
52
  }
53
53
 
54
+ private fun cleanBase64(base64: String): String {
55
+ return when {
56
+ base64.contains(",") -> base64.substringAfter(",")
57
+ else -> base64
58
+ }
59
+ }
60
+
61
+ @ReactProp(name = "baseImage")
62
+ override fun setBaseImage(view: RnStoryEditorView, base64: String?) {
63
+ base64?.let {
64
+ val cleanData = cleanBase64(it)
65
+ val bytes = android.util.Base64.decode(cleanData, android.util.Base64.DEFAULT)
66
+ val bitmap = android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
67
+ view.setBaseImage(bitmap)
68
+ }
69
+ }
70
+
71
+ @ReactProp(name = "addText")
72
+ override fun setAddText(view: RnStoryEditorView, text: String?) {
73
+ text?.let { view.addText(it) }
74
+ }
75
+
76
+ @ReactProp(name = "addImage")
77
+ override fun setAddImage(view: RnStoryEditorView, base64: String?) {
78
+ base64?.let {
79
+ val cleanData = cleanBase64(it)
80
+ view.addImage(cleanData)
81
+ }
82
+ }
83
+
84
+ // Command to export image
85
+ override fun getCommandsMap(): MutableMap<String, Int> {
86
+ return mutableMapOf("exportImage" to 1, "showTextInput" to 2)
87
+ }
88
+
89
+ override fun receiveCommand(view: RnStoryEditorView, commandId: Int, args: ReadableArray?) {
90
+ when (commandId) {
91
+ 1 -> view.exportAndSendBase64()
92
+ 2 -> view.showTextInputDialog()
93
+ }
94
+ }
95
+
54
96
  companion object {
55
97
  const val NAME = "RnStoryEditorView"
56
98
  }
@@ -7,6 +7,9 @@ import {
7
7
  interface NativeProps extends ViewProps {
8
8
  color?: ColorValue;
9
9
  colorString?: string;
10
+ baseImage?: string;
11
+ addText?: string;
12
+ addImage?: string;
10
13
  }
11
14
 
12
15
  export default codegenNativeComponent<NativeProps>('RnStoryEditorView');
@@ -7,7 +7,7 @@ const COMPONENT_NAME = 'RnStoryEditorView';
7
7
 
8
8
  export const StoryEditorView = requireNativeComponent(COMPONENT_NAME);
9
9
 
10
- // Commands
10
+ // Enhanced Commands with additional functionality
11
11
  export const StoryEditorCommands = {
12
12
  exportImage: ref => {
13
13
  const viewManagerConfig = UIManager.getViewManagerConfig(COMPONENT_NAME);
@@ -28,4 +28,12 @@ export const StoryEditorCommands = {
28
28
  }
29
29
  }
30
30
  };
31
+
32
+ // Event listener for export results
33
+ export const addExportImageListener = _callback => {
34
+ // This would be implemented with proper event handling
35
+ console.warn('Event listener not yet implemented. Use native export dialog instead.');
36
+ };
37
+
38
+ // Export types for TypeScript users
31
39
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["requireNativeComponent","UIManager","findNodeHandle","COMPONENT_NAME","StoryEditorView","StoryEditorCommands","exportImage","ref","viewManagerConfig","getViewManagerConfig","Commands","nodeHandle","dispatchViewManagerCommand","showTextInput"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,SAASA,sBAAsB,EAAEC,SAAS,EAAEC,cAAc,QAAQ,cAAc;AAGhF,MAAMC,cAAc,GAAG,mBAAmB;;AAE1C;;AAUA,OAAO,MAAMC,eAAe,GAAGJ,sBAAsB,CAAcG,cAAc,CAAC;;AAElF;AACA,OAAO,MAAME,mBAAmB,GAAG;EACjCC,WAAW,EAAGC,GAAQ,IAAK;IACzB,MAAMC,iBAAiB,GAAGP,SAAS,CAACQ,oBAAoB,CAACN,cAAc,CAA6B;IACpG,IAAIK,iBAAiB,EAAEE,QAAQ,EAAEJ,WAAW,EAAE;MAC5C,MAAMK,UAAU,GAAGT,cAAc,CAACK,GAAG,CAAC;MACtC,IAAII,UAAU,IAAI,IAAI,EAAE;QACtBV,SAAS,CAACW,0BAA0B,CAClCD,UAAU,EACVH,iBAAiB,CAACE,QAAQ,CAACJ,WAAW,EACtC,EACF,CAAC;MACH;IACF;EACF,CAAC;EAEDO,aAAa,EAAGN,GAAQ,IAAK;IAC3B,MAAMC,iBAAiB,GAAGP,SAAS,CAACQ,oBAAoB,CAACN,cAAc,CAA6B;IACpG,IAAIK,iBAAiB,EAAEE,QAAQ,EAAEG,aAAa,EAAE;MAC9C,MAAMF,UAAU,GAAGT,cAAc,CAACK,GAAG,CAAC;MACtC,IAAII,UAAU,IAAI,IAAI,EAAE;QACtBV,SAAS,CAACW,0BAA0B,CAClCD,UAAU,EACVH,iBAAiB,CAACE,QAAQ,CAACG,aAAa,EACxC,EACF,CAAC;MACH;IACF;EACF;AACF,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["requireNativeComponent","UIManager","findNodeHandle","COMPONENT_NAME","StoryEditorView","StoryEditorCommands","exportImage","ref","viewManagerConfig","getViewManagerConfig","Commands","nodeHandle","dispatchViewManagerCommand","showTextInput","addExportImageListener","_callback","console","warn"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,SAASA,sBAAsB,EAAEC,SAAS,EAAEC,cAAc,QAAQ,cAAc;AAGhF,MAAMC,cAAc,GAAG,mBAAmB;;AAE1C;;AAUA,OAAO,MAAMC,eAAe,GAAGJ,sBAAsB,CAAcG,cAAc,CAAC;;AAElF;AACA,OAAO,MAAME,mBAAmB,GAAG;EACjCC,WAAW,EAAGC,GAAQ,IAAK;IACzB,MAAMC,iBAAiB,GAAGP,SAAS,CAACQ,oBAAoB,CAACN,cAAc,CAA6B;IACpG,IAAIK,iBAAiB,EAAEE,QAAQ,EAAEJ,WAAW,EAAE;MAC5C,MAAMK,UAAU,GAAGT,cAAc,CAACK,GAAG,CAAC;MACtC,IAAII,UAAU,IAAI,IAAI,EAAE;QACtBV,SAAS,CAACW,0BAA0B,CAClCD,UAAU,EACVH,iBAAiB,CAACE,QAAQ,CAACJ,WAAW,EACtC,EACF,CAAC;MACH;IACF;EACF,CAAC;EAEDO,aAAa,EAAGN,GAAQ,IAAK;IAC3B,MAAMC,iBAAiB,GAAGP,SAAS,CAACQ,oBAAoB,CAACN,cAAc,CAA6B;IACpG,IAAIK,iBAAiB,EAAEE,QAAQ,EAAEG,aAAa,EAAE;MAC9C,MAAMF,UAAU,GAAGT,cAAc,CAACK,GAAG,CAAC;MACtC,IAAII,UAAU,IAAI,IAAI,EAAE;QACtBV,SAAS,CAACW,0BAA0B,CAClCD,UAAU,EACVH,iBAAiB,CAACE,QAAQ,CAACG,aAAa,EACxC,EACF,CAAC;MACH;IACF;EACF;AACF,CAAC;;AAED;AACA,OAAO,MAAMC,sBAAsB,GAAIC,SAAmC,IAAK;EAC7E;EACAC,OAAO,CAACC,IAAI,CAAC,uEAAuE,CAAC;AACvF,CAAC;;AAED","ignoreList":[]}
@@ -2,6 +2,9 @@ import { type ColorValue, type ViewProps } from 'react-native';
2
2
  interface NativeProps extends ViewProps {
3
3
  color?: ColorValue;
4
4
  colorString?: string;
5
+ baseImage?: string;
6
+ addText?: string;
7
+ addImage?: string;
5
8
  }
6
9
  declare const _default: import("react-native/types_generated/Libraries/Utilities/codegenNativeComponent").NativeComponentType<NativeProps>;
7
10
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"RnStoryEditorViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/RnStoryEditorViewNativeComponent.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAEtB,UAAU,WAAY,SAAQ,SAAS;IACrC,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;;AAED,wBAAwE;AAGxE,YAAY,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"RnStoryEditorViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/RnStoryEditorViewNativeComponent.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAEtB,UAAU,WAAY,SAAQ,SAAS;IACrC,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;;AAED,wBAAwE;AAGxE,YAAY,EAAE,WAAW,EAAE,CAAC"}
@@ -4,4 +4,6 @@ export declare const StoryEditorCommands: {
4
4
  exportImage: (ref: any) => void;
5
5
  showTextInput: (ref: any) => void;
6
6
  };
7
+ export declare const addExportImageListener: (_callback: (base64: string) => void) => void;
8
+ export type { NativeProps };
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AActE,eAAO,MAAM,eAAe,mDAAsD,CAAC;AAGnF,eAAO,MAAM,mBAAmB;uBACX,GAAG;yBAcD,GAAG;CAazB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AActE,eAAO,MAAM,eAAe,mDAAsD,CAAC;AAGnF,eAAO,MAAM,mBAAmB;uBACX,GAAG;yBAcD,GAAG;CAazB,CAAC;AAGF,eAAO,MAAM,sBAAsB,GAAI,WAAW,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,SAGzE,CAAC;AAGF,YAAY,EAAE,WAAW,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-native-rn-story-editor",
3
- "version": "0.1.0",
4
- "description": "This is library for the Editing tools ",
3
+ "version": "0.2.0",
4
+ "description": "A React Native story editor with layers, drag-drop, text/image overlays, and export functionality",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
7
7
  "exports": {
@@ -33,8 +33,6 @@
33
33
  ],
34
34
  "scripts": {
35
35
  "example": "yarn workspace react-native-rn-story-editor-example",
36
- "example:android": "cd example && npx react-native run-android",
37
- "example:ios": "cd example && npx react-native run-ios",
38
36
  "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
39
37
  "prepare": "bob build",
40
38
  "typecheck": "tsc",
@@ -45,7 +43,12 @@
45
43
  "keywords": [
46
44
  "react-native",
47
45
  "ios",
48
- "android"
46
+ "android",
47
+ "story-editor",
48
+ "editing-tools",
49
+ "native-components",
50
+ "fabric",
51
+ "typescript"
49
52
  ],
50
53
  "repository": {
51
54
  "type": "git",
@@ -69,7 +72,6 @@
69
72
  "@react-native/babel-preset": "0.83.0",
70
73
  "@react-native/eslint-config": "0.83.0",
71
74
  "@release-it/conventional-changelog": "^10.0.1",
72
- "@testing-library/jest-native": "^5.4.3",
73
75
  "@testing-library/react-native": "^13.3.3",
74
76
  "@types/jest": "^29.5.14",
75
77
  "@types/react": "^19.2.0",
@@ -7,6 +7,9 @@ import {
7
7
  interface NativeProps extends ViewProps {
8
8
  color?: ColorValue;
9
9
  colorString?: string;
10
+ baseImage?: string;
11
+ addText?: string;
12
+ addImage?: string;
10
13
  }
11
14
 
12
15
  export default codegenNativeComponent<NativeProps>('RnStoryEditorView');
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ interface ViewManagerConfig {
15
15
 
16
16
  export const StoryEditorView = requireNativeComponent<NativeProps>(COMPONENT_NAME);
17
17
 
18
- // Commands
18
+ // Enhanced Commands with additional functionality
19
19
  export const StoryEditorCommands = {
20
20
  exportImage: (ref: any) => {
21
21
  const viewManagerConfig = UIManager.getViewManagerConfig(COMPONENT_NAME) as ViewManagerConfig | null;
@@ -44,4 +44,13 @@ export const StoryEditorCommands = {
44
44
  }
45
45
  }
46
46
  },
47
- };
47
+ };
48
+
49
+ // Event listener for export results
50
+ export const addExportImageListener = (_callback: (base64: string) => void) => {
51
+ // This would be implemented with proper event handling
52
+ console.warn('Event listener not yet implemented. Use native export dialog instead.');
53
+ };
54
+
55
+ // Export types for TypeScript users
56
+ export type { NativeProps };