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 +130 -26
- package/android/src/main/java/com/rnstoryeditor/RnStoryEditorEventEmitter.kt +32 -0
- package/android/src/main/java/com/rnstoryeditor/RnStoryEditorPackage.kt +6 -0
- package/android/src/main/java/com/rnstoryeditor/RnStoryEditorView.kt +376 -8
- package/android/src/main/java/com/rnstoryeditor/RnStoryEditorViewManager.kt +43 -1
- package/lib/module/RnStoryEditorViewNativeComponent.ts +3 -0
- package/lib/module/index.js +9 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/RnStoryEditorViewNativeComponent.d.ts +3 -0
- package/lib/typescript/src/RnStoryEditorViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +8 -6
- package/src/RnStoryEditorViewNativeComponent.ts +3 -0
- package/src/index.ts +11 -2
package/README.md
CHANGED
|
@@ -1,30 +1,125 @@
|
|
|
1
1
|
# react-native-rn-story-editor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-rn-story-editor)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.android.com)
|
|
6
|
+
[](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
|
-
```
|
|
25
|
+
```bash
|
|
8
26
|
npm install react-native-rn-story-editor
|
|
9
27
|
```
|
|
10
28
|
|
|
11
29
|
## Usage
|
|
12
30
|
|
|
13
|
-
```
|
|
14
|
-
import {
|
|
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
|
-
|
|
17
|
-
<StoryEditorView color={0xFF32a852} style={styles.box} />
|
|
95
|
+
## Events
|
|
18
96
|
|
|
19
|
-
|
|
20
|
-
<StoryEditorView colorString="#32a852" style={styles.box} />
|
|
97
|
+
### onExportImage
|
|
21
98
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
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
|
}
|
package/lib/module/index.js
CHANGED
|
@@ -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
|
package/lib/module/index.js.map
CHANGED
|
@@ -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;
|
|
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.
|
|
4
|
-
"description": "
|
|
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",
|
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 };
|