react-native-rn-story-editor 0.2.1 → 0.2.2
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 +39 -28
- package/android/src/main/java/com/rnstoryeditor/RnStoryEditorEventEmitter.kt +86 -5
- package/android/src/main/java/com/rnstoryeditor/RnStoryEditorView.kt +136 -95
- package/android/src/main/java/com/rnstoryeditor/RnStoryEditorViewManager.kt +48 -8
- package/android/src/main/java/com/rnstoryeditor/StoryEditorErrors.kt +169 -0
- package/lib/module/errors.js +89 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +179 -19
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/errors.d.ts +46 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +53 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +154 -0
- package/src/index.ts +246 -31
package/README.md
CHANGED
|
@@ -31,38 +31,44 @@ npm install react-native-rn-story-editor
|
|
|
31
31
|
```jsx
|
|
32
32
|
import { useRef, useEffect } from 'react';
|
|
33
33
|
import { View, StyleSheet, DeviceEventEmitter } from 'react-native';
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
StoryEditorView,
|
|
36
|
+
StoryEditorCommands,
|
|
37
|
+
} from 'react-native-rn-story-editor';
|
|
35
38
|
|
|
36
39
|
export default function App() {
|
|
37
40
|
const editorRef = useRef(null);
|
|
38
41
|
|
|
39
42
|
useEffect(() => {
|
|
40
43
|
// Listen for export events
|
|
41
|
-
const subscription = DeviceEventEmitter.addListener(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const subscription = DeviceEventEmitter.addListener(
|
|
45
|
+
'onExportImage',
|
|
46
|
+
(event) => {
|
|
47
|
+
console.log('Exported image base64:', event.image);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
44
50
|
return () => subscription.remove();
|
|
45
51
|
}, []);
|
|
46
52
|
|
|
47
53
|
return (
|
|
48
54
|
<View style={styles.container}>
|
|
49
|
-
<StoryEditorView
|
|
55
|
+
<StoryEditorView
|
|
50
56
|
ref={editorRef}
|
|
51
57
|
colorString="#32a852"
|
|
52
58
|
baseImage="data:image/png;base64,iVBORw0KGgo..."
|
|
53
59
|
style={styles.editor}
|
|
54
60
|
/>
|
|
55
|
-
|
|
61
|
+
|
|
56
62
|
{/* Export to gallery or get base64 */}
|
|
57
|
-
<Button
|
|
58
|
-
title="Export Image"
|
|
59
|
-
onPress={() => StoryEditorCommands.exportImage(editorRef.current)}
|
|
63
|
+
<Button
|
|
64
|
+
title="Export Image"
|
|
65
|
+
onPress={() => StoryEditorCommands.exportImage(editorRef.current)}
|
|
60
66
|
/>
|
|
61
|
-
|
|
67
|
+
|
|
62
68
|
{/* Show text input dialog */}
|
|
63
|
-
<Button
|
|
64
|
-
title="Add Text"
|
|
65
|
-
onPress={() => StoryEditorCommands.showTextInput(editorRef.current)}
|
|
69
|
+
<Button
|
|
70
|
+
title="Add Text"
|
|
71
|
+
onPress={() => StoryEditorCommands.showTextInput(editorRef.current)}
|
|
66
72
|
/>
|
|
67
73
|
</View>
|
|
68
74
|
);
|
|
@@ -70,27 +76,27 @@ export default function App() {
|
|
|
70
76
|
|
|
71
77
|
const styles = StyleSheet.create({
|
|
72
78
|
container: { flex: 1 },
|
|
73
|
-
editor: { width: 300, height: 400 }
|
|
79
|
+
editor: { width: 300, height: 400 },
|
|
74
80
|
});
|
|
75
81
|
```
|
|
76
82
|
|
|
77
83
|
## Props
|
|
78
84
|
|
|
79
|
-
| Prop
|
|
80
|
-
|
|
81
|
-
| `color`
|
|
82
|
-
| `colorString` | `string`
|
|
83
|
-
| `baseImage`
|
|
84
|
-
| `addText`
|
|
85
|
-
| `addImage`
|
|
86
|
-
| `style`
|
|
85
|
+
| Prop | Type | Default | Description |
|
|
86
|
+
| ------------- | ----------- | ------------------- | ---------------------------------------------------------- |
|
|
87
|
+
| `color` | `number` | `Color.TRANSPARENT` | Numeric color value (e.g., `0xFF32a852`) |
|
|
88
|
+
| `colorString` | `string` | `undefined` | Hex color string (e.g., `"#32a852"`) |
|
|
89
|
+
| `baseImage` | `string` | `undefined` | Base64 encoded background image (supports data URI format) |
|
|
90
|
+
| `addText` | `string` | `undefined` | Add text layer programmatically |
|
|
91
|
+
| `addImage` | `string` | `undefined` | Add overlay image from base64 (supports data URI format) |
|
|
92
|
+
| `style` | `ViewStyle` | `undefined` | React Native style object |
|
|
87
93
|
|
|
88
94
|
## Commands
|
|
89
95
|
|
|
90
|
-
| Command
|
|
91
|
-
|
|
92
|
-
| `exportImage`
|
|
93
|
-
| `showTextInput` | `(ref: React.Ref)` | Shows native text input dialog to add text layer
|
|
96
|
+
| Command | Parameters | Description |
|
|
97
|
+
| --------------- | ------------------ | ---------------------------------------------------------- |
|
|
98
|
+
| `exportImage` | `(ref: React.Ref)` | Shows export dialog with gallery save and database options |
|
|
99
|
+
| `showTextInput` | `(ref: React.Ref)` | Shows native text input dialog to add text layer |
|
|
94
100
|
|
|
95
101
|
## Events
|
|
96
102
|
|
|
@@ -123,16 +129,18 @@ When `exportImage` is called, users see a dialog with:
|
|
|
123
129
|
## Testing
|
|
124
130
|
|
|
125
131
|
### Unit Tests
|
|
132
|
+
|
|
126
133
|
```bash
|
|
127
134
|
npm test
|
|
128
135
|
```
|
|
129
136
|
|
|
130
137
|
### Manual Testing
|
|
138
|
+
|
|
131
139
|
```bash
|
|
132
140
|
# Test on Android
|
|
133
141
|
npm run example:android
|
|
134
142
|
|
|
135
|
-
# Test on iOS
|
|
143
|
+
# Test on iOS
|
|
136
144
|
npm run example:ios
|
|
137
145
|
|
|
138
146
|
# Test different React Native versions
|
|
@@ -142,8 +150,9 @@ npm install react-native@0.73
|
|
|
142
150
|
```
|
|
143
151
|
|
|
144
152
|
### Cross-Platform Verification
|
|
153
|
+
|
|
145
154
|
- Android: Tested on API 24+ (emulators/devices)
|
|
146
|
-
- iOS: Test on iOS 12+ (simulators/devices)
|
|
155
|
+
- iOS: Test on iOS 12+ (simulators/devices)
|
|
147
156
|
- React Native: Test with multiple versions
|
|
148
157
|
- TypeScript: All types compile correctly
|
|
149
158
|
- Props: Color, style, baseImage, addText, addImage
|
|
@@ -153,6 +162,7 @@ npm install react-native@0.73
|
|
|
153
162
|
## Version History
|
|
154
163
|
|
|
155
164
|
### 0.2.0
|
|
165
|
+
|
|
156
166
|
- Added layer management (text and image layers)
|
|
157
167
|
- Added drag-and-drop functionality
|
|
158
168
|
- Added base64 image support for backgrounds and overlays
|
|
@@ -162,6 +172,7 @@ npm install react-native@0.73
|
|
|
162
172
|
- Improved event system for JavaScript communication
|
|
163
173
|
|
|
164
174
|
### 0.1.0
|
|
175
|
+
|
|
165
176
|
- Initial release
|
|
166
177
|
- Basic view component with color support
|
|
167
178
|
- Native commands structure
|
|
@@ -6,7 +6,12 @@ import com.facebook.react.bridge.ReactMethod
|
|
|
6
6
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
7
7
|
import com.facebook.react.bridge.Arguments
|
|
8
8
|
import com.facebook.react.bridge.WritableMap
|
|
9
|
+
import android.util.Log
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Event emitter for Story Editor
|
|
13
|
+
* Handles both success events and error events
|
|
14
|
+
*/
|
|
10
15
|
class RnStoryEditorEventEmitter(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
11
16
|
|
|
12
17
|
override fun getName(): String {
|
|
@@ -14,19 +19,95 @@ class RnStoryEditorEventEmitter(reactContext: ReactApplicationContext) : ReactCo
|
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
companion object {
|
|
22
|
+
private const val TAG = "StoryEditorEvent"
|
|
17
23
|
private var reactAppContext: ReactApplicationContext? = null
|
|
18
24
|
|
|
19
25
|
fun setReactContext(context: ReactApplicationContext) {
|
|
20
26
|
reactAppContext = context
|
|
27
|
+
Log.d(TAG, "React context set successfully")
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Emits a success event with base64 image data
|
|
32
|
+
*/
|
|
23
33
|
fun emitEvent(eventName: String, base64: String) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
try {
|
|
35
|
+
val params = Arguments.createMap().apply {
|
|
36
|
+
putString("base64", base64)
|
|
37
|
+
putNull("error")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
reactAppContext?.let { ctx ->
|
|
41
|
+
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
42
|
+
.emit(eventName, params)
|
|
43
|
+
Log.d(TAG, "Event emitted successfully: $eventName")
|
|
44
|
+
} ?: run {
|
|
45
|
+
Log.e(TAG, "Cannot emit event - React context is null")
|
|
46
|
+
}
|
|
47
|
+
} catch (e: Exception) {
|
|
48
|
+
Log.e(TAG, "Failed to emit event: ${e.message}")
|
|
49
|
+
emitError(StoryEditorError.unknownError("emitEvent", e))
|
|
29
50
|
}
|
|
30
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Emits an error event to JavaScript
|
|
55
|
+
*/
|
|
56
|
+
fun emitError(error: StoryEditorError) {
|
|
57
|
+
try {
|
|
58
|
+
val params = Arguments.createMap().apply {
|
|
59
|
+
putString("code", error.code)
|
|
60
|
+
putString("message", error.message)
|
|
61
|
+
error.nativeError?.let {
|
|
62
|
+
putString("nativeError", it.message)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
reactAppContext?.let { ctx ->
|
|
67
|
+
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
68
|
+
.emit("onStoryEditorError", params)
|
|
69
|
+
Log.e(TAG, "Error emitted [${error.code}]: ${error.message}")
|
|
70
|
+
} ?: run {
|
|
71
|
+
Log.e(TAG, "Cannot emit error - React context is null. Error: [${error.code}] ${error.message}")
|
|
72
|
+
}
|
|
73
|
+
} catch (e: Exception) {
|
|
74
|
+
Log.e(TAG, "Failed to emit error event: ${e.message}")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convenience method to emit export success
|
|
80
|
+
*/
|
|
81
|
+
fun emitExportSuccess(base64: String) {
|
|
82
|
+
emitEvent("onExportImage", base64)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Convenience method to emit export error
|
|
87
|
+
*/
|
|
88
|
+
fun emitExportError(error: StoryEditorError) {
|
|
89
|
+
try {
|
|
90
|
+
val params = Arguments.createMap().apply {
|
|
91
|
+
putString("base64", "")
|
|
92
|
+
putString("errorCode", error.code)
|
|
93
|
+
putString("errorMessage", error.message)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
reactAppContext?.let { ctx ->
|
|
97
|
+
ctx.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
98
|
+
.emit("onExportImage", params)
|
|
99
|
+
Log.e(TAG, "Export error emitted [${error.code}]: ${error.message}")
|
|
100
|
+
}
|
|
101
|
+
} catch (e: Exception) {
|
|
102
|
+
Log.e(TAG, "Failed to emit export error: ${e.message}")
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if react context is available
|
|
108
|
+
*/
|
|
109
|
+
fun hasReactContext(): Boolean {
|
|
110
|
+
return reactAppContext != null
|
|
111
|
+
}
|
|
31
112
|
}
|
|
32
113
|
}
|
|
@@ -67,10 +67,14 @@ class RnStoryEditorView(context: Context, attrs: AttributeSet? = null) : View(co
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
fun addImage(base64: String) {
|
|
70
|
-
|
|
71
|
-
val bytes = Base64.decode(
|
|
70
|
+
safeStoryEditorCall("addImage") {
|
|
71
|
+
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
|
72
72
|
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
73
73
|
|
|
74
|
+
if (bitmap == null) {
|
|
75
|
+
throw StoryEditorError.bitmapDecodeFailed("Failed to decode base64 to bitmap")
|
|
76
|
+
}
|
|
77
|
+
|
|
74
78
|
// Scale image to reasonable size (max 200dp width)
|
|
75
79
|
val maxWidth = 200f * resources.displayMetrics.density
|
|
76
80
|
val scale = maxWidth / bitmap.width
|
|
@@ -85,29 +89,45 @@ class RnStoryEditorView(context: Context, attrs: AttributeSet? = null) : View(co
|
|
|
85
89
|
|
|
86
90
|
layers.add(Layer.ImageLayer(scaledBitmap, x, y, scaledWidth, scaledHeight))
|
|
87
91
|
invalidate()
|
|
92
|
+
}.onError { error ->
|
|
93
|
+
RnStoryEditorEventEmitter.emitError(error)
|
|
88
94
|
}
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
fun showTextInputDialog() {
|
|
92
98
|
Log.d("RnStoryEditorView", "showTextInputDialog called")
|
|
93
|
-
val editText = EditText(context)
|
|
94
|
-
editText.hint = "Enter text here"
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
try {
|
|
101
|
+
val editText = EditText(context)
|
|
102
|
+
editText.hint = "Enter text here"
|
|
103
|
+
|
|
104
|
+
AlertDialog.Builder(context)
|
|
105
|
+
.setTitle("Add Text Layer")
|
|
106
|
+
.setView(editText)
|
|
107
|
+
.setPositiveButton("Add") { dialog, _ ->
|
|
108
|
+
val inputText = editText.text.toString()
|
|
109
|
+
Log.d("RnStoryEditorView", "Input text: $inputText")
|
|
110
|
+
if (inputText.isNotEmpty()) {
|
|
111
|
+
try {
|
|
112
|
+
addText(inputText)
|
|
113
|
+
} catch (e: Exception) {
|
|
114
|
+
RnStoryEditorEventEmitter.emitError(
|
|
115
|
+
StoryEditorError.textInputFailed("Failed to add text: ${e.message}")
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
dialog.dismiss()
|
|
104
120
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
.
|
|
121
|
+
.setNegativeButton("Cancel") { dialog, _ ->
|
|
122
|
+
dialog.dismiss()
|
|
123
|
+
}
|
|
124
|
+
.show()
|
|
125
|
+
} catch (e: Exception) {
|
|
126
|
+
Log.e("RnStoryEditorView", "Error showing text input dialog: ${e.message}")
|
|
127
|
+
RnStoryEditorEventEmitter.emitError(
|
|
128
|
+
StoryEditorError.textInputFailed("Dialog creation failed: ${e.message}")
|
|
129
|
+
)
|
|
130
|
+
}
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
fun showImageOptionsDialog(imageLayer: Layer.ImageLayer) {
|
|
@@ -261,8 +281,8 @@ class RnStoryEditorView(context: Context, attrs: AttributeSet? = null) : View(co
|
|
|
261
281
|
return true
|
|
262
282
|
}
|
|
263
283
|
|
|
264
|
-
fun saveImageToGallery(bitmap: Bitmap):
|
|
265
|
-
return
|
|
284
|
+
fun saveImageToGallery(bitmap: Bitmap): StoryEditorResult<String> {
|
|
285
|
+
return safeStoryEditorCall("saveImageToGallery") {
|
|
266
286
|
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
|
267
287
|
val filename = "StoryEditor_$timestamp.png"
|
|
268
288
|
|
|
@@ -276,23 +296,27 @@ class RnStoryEditorView(context: Context, attrs: AttributeSet? = null) : View(co
|
|
|
276
296
|
}
|
|
277
297
|
|
|
278
298
|
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
299
|
+
?: throw StoryEditorError.fileSaveFailed("Failed to create MediaStore entry")
|
|
300
|
+
|
|
301
|
+
resolver.openOutputStream(uri)?.use { stream ->
|
|
302
|
+
if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
|
|
303
|
+
throw StoryEditorError.fileSaveFailed("Bitmap compression failed")
|
|
283
304
|
}
|
|
284
|
-
}
|
|
305
|
+
} ?: throw StoryEditorError.fileSaveFailed("Failed to open output stream")
|
|
306
|
+
|
|
307
|
+
uri.toString()
|
|
285
308
|
} else {
|
|
286
309
|
// Android 9 and below use File
|
|
287
310
|
val directory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "StoryEditor")
|
|
288
|
-
if (!directory.exists()) {
|
|
289
|
-
|
|
311
|
+
if (!directory.exists() && !directory.mkdirs()) {
|
|
312
|
+
throw StoryEditorError.fileSaveFailed("Failed to create directory")
|
|
290
313
|
}
|
|
291
314
|
|
|
292
315
|
val imageFile = File(directory, filename)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
316
|
+
FileOutputStream(imageFile).use { stream ->
|
|
317
|
+
if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
|
|
318
|
+
throw StoryEditorError.fileSaveFailed("Bitmap compression failed")
|
|
319
|
+
}
|
|
296
320
|
}
|
|
297
321
|
|
|
298
322
|
// Add to gallery
|
|
@@ -301,83 +325,100 @@ class RnStoryEditorView(context: Context, attrs: AttributeSet? = null) : View(co
|
|
|
301
325
|
put(MediaStore.Images.Media.MIME_TYPE, "image/png")
|
|
302
326
|
}
|
|
303
327
|
context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
|
328
|
+
imageFile.absolutePath
|
|
329
|
+
}.also {
|
|
330
|
+
Log.d("RnStoryEditorView", "Image saved to gallery: $filename")
|
|
304
331
|
}
|
|
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
332
|
}
|
|
312
333
|
}
|
|
313
334
|
|
|
314
335
|
// Merge layers and send base64 back to JS
|
|
315
336
|
fun exportAndSendBase64() {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
337
|
+
safeStoryEditorCall("exportAndSendBase64") {
|
|
338
|
+
if (width <= 0 || height <= 0) {
|
|
339
|
+
throw StoryEditorError.imageExportFailed("View has invalid dimensions: ${width}x${height}")
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
343
|
+
val canvas = Canvas(bitmap)
|
|
344
|
+
draw(canvas)
|
|
319
345
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
346
|
+
val outputStream = ByteArrayOutputStream()
|
|
347
|
+
if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)) {
|
|
348
|
+
throw StoryEditorError.imageExportFailed("Failed to compress bitmap to PNG")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
val byteArray = outputStream.toByteArray()
|
|
352
|
+
val base64 = Base64.encodeToString(byteArray, Base64.NO_WRAP)
|
|
324
353
|
|
|
325
|
-
|
|
326
|
-
|
|
354
|
+
// Show export dialog with download and database options
|
|
355
|
+
showExportPreviewDialog(bitmap, base64)
|
|
356
|
+
}.onError { error ->
|
|
357
|
+
RnStoryEditorEventEmitter.emitExportError(error)
|
|
358
|
+
}
|
|
327
359
|
}
|
|
328
360
|
|
|
329
361
|
fun showExportPreviewDialog(bitmap: Bitmap, base64: String) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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}
|
|
362
|
+
try {
|
|
363
|
+
val imageView = ImageView(context)
|
|
364
|
+
imageView.setImageBitmap(bitmap)
|
|
365
|
+
imageView.setPadding(20, 20, 20, 20)
|
|
348
366
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
val infoText = TextView(context)
|
|
368
|
+
val sizeKB = base64.length / 1024
|
|
369
|
+
val sizeMB = sizeKB / 1024.0
|
|
370
|
+
val imageCount = layers.count { it is Layer.ImageLayer }
|
|
371
|
+
val textCount = layers.count { it is Layer.TextLayer }
|
|
372
|
+
infoText.text = """
|
|
373
|
+
Export Information:
|
|
374
|
+
• Image Size: ${bitmap.width} x ${bitmap.height}px
|
|
375
|
+
• File Size: ${sizeKB} KB (${String.format("%.2f", sizeMB)} MB)
|
|
376
|
+
• Base64 Length: ${base64.length} characters
|
|
377
|
+
• Format: PNG with Base64 encoding
|
|
378
|
+
• Image Layers: $imageCount
|
|
379
|
+
• Text Layers: $textCount
|
|
380
|
+
• Total Layers: ${layers.size}
|
|
381
|
+
|
|
382
|
+
Choose export option below.
|
|
383
|
+
""".trimIndent()
|
|
384
|
+
infoText.setPadding(20, 10, 20, 20)
|
|
385
|
+
infoText.textSize = 14f
|
|
386
|
+
|
|
387
|
+
val scrollView = ScrollView(context)
|
|
388
|
+
val layout = android.widget.LinearLayout(context)
|
|
389
|
+
layout.orientation = android.widget.LinearLayout.VERTICAL
|
|
390
|
+
layout.addView(imageView)
|
|
391
|
+
layout.addView(infoText)
|
|
392
|
+
scrollView.addView(layout)
|
|
393
|
+
|
|
394
|
+
AlertDialog.Builder(context)
|
|
395
|
+
.setTitle("Export Options")
|
|
396
|
+
.setView(scrollView)
|
|
397
|
+
.setPositiveButton("Download to Gallery") { dialog, _ ->
|
|
398
|
+
saveImageToGallery(bitmap)
|
|
399
|
+
.onSuccess { path ->
|
|
400
|
+
android.widget.Toast.makeText(context, "Image saved to gallery!", android.widget.Toast.LENGTH_SHORT).show()
|
|
401
|
+
}
|
|
402
|
+
.onError { error ->
|
|
403
|
+
android.widget.Toast.makeText(context, "Failed to save image: ${error.message}", android.widget.Toast.LENGTH_LONG).show()
|
|
404
|
+
RnStoryEditorEventEmitter.emitError(error)
|
|
405
|
+
}
|
|
406
|
+
dialog.dismiss()
|
|
370
407
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
.
|
|
408
|
+
.setNeutralButton("Send to Database") { dialog, _ ->
|
|
409
|
+
// Send event to JS with base64
|
|
410
|
+
RnStoryEditorEventEmitter.emitExportSuccess(base64)
|
|
411
|
+
dialog.dismiss()
|
|
412
|
+
}
|
|
413
|
+
.setNegativeButton("Cancel") { dialog, _ ->
|
|
414
|
+
dialog.dismiss()
|
|
415
|
+
}
|
|
416
|
+
.show()
|
|
417
|
+
} catch (e: Exception) {
|
|
418
|
+
Log.e("RnStoryEditorView", "Error showing export dialog: ${e.message}")
|
|
419
|
+
RnStoryEditorEventEmitter.emitExportError(
|
|
420
|
+
StoryEditorError.imageExportFailed("Failed to show export dialog: ${e.message}", e)
|
|
421
|
+
)
|
|
422
|
+
}
|
|
382
423
|
}
|
|
383
424
|
}
|
|
@@ -61,10 +61,28 @@ class RnStoryEditorViewManager : SimpleViewManager<RnStoryEditorView>(),
|
|
|
61
61
|
@ReactProp(name = "baseImage")
|
|
62
62
|
override fun setBaseImage(view: RnStoryEditorView, base64: String?) {
|
|
63
63
|
base64?.let {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
try {
|
|
65
|
+
val cleanData = cleanBase64(it)
|
|
66
|
+
val bytes = android.util.Base64.decode(cleanData, android.util.Base64.DEFAULT)
|
|
67
|
+
val bitmap = android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
68
|
+
|
|
69
|
+
if (bitmap == null) {
|
|
70
|
+
RnStoryEditorEventEmitter.emitError(
|
|
71
|
+
StoryEditorError.bitmapDecodeFailed("Decoded bitmap is null, invalid base64 data")
|
|
72
|
+
)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
view.setBaseImage(bitmap)
|
|
77
|
+
} catch (e: IllegalArgumentException) {
|
|
78
|
+
RnStoryEditorEventEmitter.emitError(StoryEditorError.invalidBase64())
|
|
79
|
+
} catch (e: OutOfMemoryError) {
|
|
80
|
+
RnStoryEditorEventEmitter.emitError(StoryEditorError.memoryError("decode base64", e))
|
|
81
|
+
} catch (e: Exception) {
|
|
82
|
+
RnStoryEditorEventEmitter.emitError(
|
|
83
|
+
StoryEditorError.bitmapDecodeFailed(e.message ?: "Unknown error")
|
|
84
|
+
)
|
|
85
|
+
}
|
|
68
86
|
}
|
|
69
87
|
}
|
|
70
88
|
|
|
@@ -76,8 +94,18 @@ class RnStoryEditorViewManager : SimpleViewManager<RnStoryEditorView>(),
|
|
|
76
94
|
@ReactProp(name = "addImage")
|
|
77
95
|
override fun setAddImage(view: RnStoryEditorView, base64: String?) {
|
|
78
96
|
base64?.let {
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
try {
|
|
98
|
+
val cleanData = cleanBase64(it)
|
|
99
|
+
view.addImage(cleanData)
|
|
100
|
+
} catch (e: IllegalArgumentException) {
|
|
101
|
+
RnStoryEditorEventEmitter.emitError(StoryEditorError.invalidBase64())
|
|
102
|
+
} catch (e: StoryEditorError) {
|
|
103
|
+
RnStoryEditorEventEmitter.emitError(e)
|
|
104
|
+
} catch (e: Exception) {
|
|
105
|
+
RnStoryEditorEventEmitter.emitError(
|
|
106
|
+
StoryEditorError.layerOperationFailed("add image", e.message ?: "Unknown error")
|
|
107
|
+
)
|
|
108
|
+
}
|
|
81
109
|
}
|
|
82
110
|
}
|
|
83
111
|
|
|
@@ -88,8 +116,20 @@ class RnStoryEditorViewManager : SimpleViewManager<RnStoryEditorView>(),
|
|
|
88
116
|
|
|
89
117
|
override fun receiveCommand(view: RnStoryEditorView, commandId: Int, args: ReadableArray?) {
|
|
90
118
|
when (commandId) {
|
|
91
|
-
1 ->
|
|
92
|
-
|
|
119
|
+
1 -> {
|
|
120
|
+
safeStoryEditorCall("exportImage") {
|
|
121
|
+
view.exportAndSendBase64()
|
|
122
|
+
}.onError { error ->
|
|
123
|
+
RnStoryEditorEventEmitter.emitExportError(error)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
2 -> {
|
|
127
|
+
safeStoryEditorCall("showTextInput") {
|
|
128
|
+
view.showTextInputDialog()
|
|
129
|
+
}.onError { error ->
|
|
130
|
+
RnStoryEditorEventEmitter.emitError(error)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
93
133
|
}
|
|
94
134
|
}
|
|
95
135
|
|