react-native-photo-video-editor 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/PhotoVideoEditor.podspec +1 -0
- package/README.md +10 -6
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/android/build.gradle +1 -1
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/assets/Stickers/AUTUMN_1.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_10.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_11.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_12.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_13.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_14.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_15.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_16.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_17.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_18.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_2.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_3.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_4.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_5.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_6.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_7.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_8.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_9.png +0 -0
- package/android/src/main/assets/Stickers/LOL_1.png +0 -0
- package/android/src/main/assets/Stickers/LOL_10.png +0 -0
- package/android/src/main/assets/Stickers/LOL_11.png +0 -0
- package/android/src/main/assets/Stickers/LOL_12.png +0 -0
- package/android/src/main/assets/Stickers/LOL_13.png +0 -0
- package/android/src/main/assets/Stickers/LOL_14.png +0 -0
- package/android/src/main/assets/Stickers/LOL_15.png +0 -0
- package/android/src/main/assets/Stickers/LOL_16.png +0 -0
- package/android/src/main/assets/Stickers/LOL_17.png +0 -0
- package/android/src/main/assets/Stickers/LOL_18.png +0 -0
- package/android/src/main/assets/Stickers/LOL_19.png +0 -0
- package/android/src/main/assets/Stickers/LOL_2.png +0 -0
- package/android/src/main/assets/Stickers/LOL_3.png +0 -0
- package/android/src/main/assets/Stickers/LOL_4.png +0 -0
- package/android/src/main/assets/Stickers/LOL_5.png +0 -0
- package/android/src/main/assets/Stickers/LOL_6.png +0 -0
- package/android/src/main/assets/Stickers/LOL_7.png +0 -0
- package/android/src/main/assets/Stickers/LOL_8.png +0 -0
- package/android/src/main/assets/Stickers/LOL_9.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_1.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_2.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_3.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_4.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_5.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_6.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_1.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_10.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_11.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_12.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_13.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_14.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_15.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_16.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_17.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_18.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_19.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_2.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_20.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_21.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_3.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_4.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_5.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_6.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_7.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_8.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_9.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_1.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_12.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_13.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_14.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_15.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_16.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_2.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_25.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_26.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_27.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_28.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_29.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_3.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_30.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_31.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_32.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_7.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_8.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_9.png +0 -0
- package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +188 -108
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayer.kt +5 -1
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +21 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +34 -2
- package/android/src/main/java/com/photovideoeditor/photo/ui/DrawOverlayView.kt +66 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +21 -1
- package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +135 -104
- package/android/src/main/res/drawable/ic_draw.xml +6 -0
- package/android/src/main/res/drawable/ic_pause.xml +7 -0
- package/android/src/main/res/drawable/ic_play.xml +7 -0
- package/docs/api.md +1 -1
- package/docs/photo-editor.md +5 -6
- package/ios/Editor/PhotoVideoEditorViewController.swift +166 -77
- package/ios/Photo/Rendering/PhotoLayer.swift +5 -1
- package/ios/Photo/Rendering/PhotoLayerRenderer.swift +14 -0
- package/ios/Photo/UI/CropOverlayView.swift +38 -1
- package/ios/Photo/UI/DrawOverlayView.swift +48 -0
- package/ios/Photo/UI/LayerOverlayView.swift +9 -1
- package/ios/Resources/Stickers/AUTUMN_1.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_10.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_11.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_12.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_13.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_14.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_15.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_16.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_17.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_18.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_2.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_3.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_4.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_5.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_6.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_7.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_8.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_9.png +0 -0
- package/ios/Resources/Stickers/LOL_1.png +0 -0
- package/ios/Resources/Stickers/LOL_10.png +0 -0
- package/ios/Resources/Stickers/LOL_11.png +0 -0
- package/ios/Resources/Stickers/LOL_12.png +0 -0
- package/ios/Resources/Stickers/LOL_13.png +0 -0
- package/ios/Resources/Stickers/LOL_14.png +0 -0
- package/ios/Resources/Stickers/LOL_15.png +0 -0
- package/ios/Resources/Stickers/LOL_16.png +0 -0
- package/ios/Resources/Stickers/LOL_17.png +0 -0
- package/ios/Resources/Stickers/LOL_18.png +0 -0
- package/ios/Resources/Stickers/LOL_19.png +0 -0
- package/ios/Resources/Stickers/LOL_2.png +0 -0
- package/ios/Resources/Stickers/LOL_3.png +0 -0
- package/ios/Resources/Stickers/LOL_4.png +0 -0
- package/ios/Resources/Stickers/LOL_5.png +0 -0
- package/ios/Resources/Stickers/LOL_6.png +0 -0
- package/ios/Resources/Stickers/LOL_7.png +0 -0
- package/ios/Resources/Stickers/LOL_8.png +0 -0
- package/ios/Resources/Stickers/LOL_9.png +0 -0
- package/ios/Resources/Stickers/SPRING_1.png +0 -0
- package/ios/Resources/Stickers/SPRING_2.png +0 -0
- package/ios/Resources/Stickers/SPRING_3.png +0 -0
- package/ios/Resources/Stickers/SPRING_4.png +0 -0
- package/ios/Resources/Stickers/SPRING_5.png +0 -0
- package/ios/Resources/Stickers/SPRING_6.png +0 -0
- package/ios/Resources/Stickers/SUMMER_1.png +0 -0
- package/ios/Resources/Stickers/SUMMER_10.png +0 -0
- package/ios/Resources/Stickers/SUMMER_11.png +0 -0
- package/ios/Resources/Stickers/SUMMER_12.png +0 -0
- package/ios/Resources/Stickers/SUMMER_13.png +0 -0
- package/ios/Resources/Stickers/SUMMER_14.png +0 -0
- package/ios/Resources/Stickers/SUMMER_15.png +0 -0
- package/ios/Resources/Stickers/SUMMER_16.png +0 -0
- package/ios/Resources/Stickers/SUMMER_17.png +0 -0
- package/ios/Resources/Stickers/SUMMER_18.png +0 -0
- package/ios/Resources/Stickers/SUMMER_19.png +0 -0
- package/ios/Resources/Stickers/SUMMER_2.png +0 -0
- package/ios/Resources/Stickers/SUMMER_20.png +0 -0
- package/ios/Resources/Stickers/SUMMER_21.png +0 -0
- package/ios/Resources/Stickers/SUMMER_3.png +0 -0
- package/ios/Resources/Stickers/SUMMER_4.png +0 -0
- package/ios/Resources/Stickers/SUMMER_5.png +0 -0
- package/ios/Resources/Stickers/SUMMER_6.png +0 -0
- package/ios/Resources/Stickers/SUMMER_7.png +0 -0
- package/ios/Resources/Stickers/SUMMER_8.png +0 -0
- package/ios/Resources/Stickers/SUMMER_9.png +0 -0
- package/ios/Resources/Stickers/TEXT_1.png +0 -0
- package/ios/Resources/Stickers/TEXT_12.png +0 -0
- package/ios/Resources/Stickers/TEXT_13.png +0 -0
- package/ios/Resources/Stickers/TEXT_14.png +0 -0
- package/ios/Resources/Stickers/TEXT_15.png +0 -0
- package/ios/Resources/Stickers/TEXT_16.png +0 -0
- package/ios/Resources/Stickers/TEXT_2.png +0 -0
- package/ios/Resources/Stickers/TEXT_25.png +0 -0
- package/ios/Resources/Stickers/TEXT_26.png +0 -0
- package/ios/Resources/Stickers/TEXT_27.png +0 -0
- package/ios/Resources/Stickers/TEXT_28.png +0 -0
- package/ios/Resources/Stickers/TEXT_29.png +0 -0
- package/ios/Resources/Stickers/TEXT_3.png +0 -0
- package/ios/Resources/Stickers/TEXT_30.png +0 -0
- package/ios/Resources/Stickers/TEXT_31.png +0 -0
- package/ios/Resources/Stickers/TEXT_32.png +0 -0
- package/ios/Resources/Stickers/TEXT_7.png +0 -0
- package/ios/Resources/Stickers/TEXT_8.png +0 -0
- package/ios/Resources/Stickers/TEXT_9.png +0 -0
- package/ios/Resources/Stickers/a_pety_icon.png +0 -0
- package/ios/Stickers/OnlineStickerSheetViewController.swift +102 -159
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/types.d.ts +2 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +1 -1
- package/src/types.ts +2 -2
- package/android/src/main/java/com/photovideoeditor/stickers/OpenMojiCatalog.kt +0 -172
- package/android/src/main/java/com/photovideoeditor/stickers/RemoteImageLoader.kt +0 -110
- package/ios/Stickers/OpenMojiCatalog.swift +0 -188
- package/ios/Stickers/RemoteImageLoader.swift +0 -127
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package com.photovideoeditor.photo.ui
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.graphics.Paint
|
|
7
|
+
import android.graphics.Path
|
|
8
|
+
import android.graphics.RectF
|
|
9
|
+
import android.view.MotionEvent
|
|
10
|
+
import android.view.View
|
|
11
|
+
|
|
12
|
+
data class DrawStroke(val points: MutableList<Pair<Float, Float>>, val color: Int, val widthPx: Float)
|
|
13
|
+
|
|
14
|
+
/** Captures freehand strokes over the visible image while Draw mode is active. */
|
|
15
|
+
class DrawOverlayView(context: Context) : View(context) {
|
|
16
|
+
private var imageBounds = RectF()
|
|
17
|
+
var strokeColor: Int = Color.RED
|
|
18
|
+
var strokeWidthPx: Float = 12f
|
|
19
|
+
private val strokes = mutableListOf<DrawStroke>()
|
|
20
|
+
private var current: DrawStroke? = null
|
|
21
|
+
var onStrokesChanged: ((List<DrawStroke>) -> Unit)? = null
|
|
22
|
+
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
23
|
+
style = Paint.Style.STROKE
|
|
24
|
+
strokeCap = Paint.Cap.ROUND
|
|
25
|
+
strokeJoin = Paint.Join.ROUND
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fun setImageBounds(bounds: RectF) { imageBounds = RectF(bounds) }
|
|
29
|
+
fun clearStrokes() { strokes.clear(); current = null; invalidate(); onStrokesChanged?.invoke(strokes) }
|
|
30
|
+
fun undoLastStroke() { if (strokes.isNotEmpty()) strokes.removeAt(strokes.lastIndex); invalidate(); onStrokesChanged?.invoke(strokes) }
|
|
31
|
+
val hasStrokes: Boolean get() = strokes.isNotEmpty()
|
|
32
|
+
|
|
33
|
+
fun normalizedStrokes(): List<Pair<List<Pair<Float, Float>>, DrawStroke>> {
|
|
34
|
+
if (imageBounds.width() <= 0 || imageBounds.height() <= 0) return emptyList()
|
|
35
|
+
return strokes.map { stroke ->
|
|
36
|
+
stroke.points.map { (x, y) ->
|
|
37
|
+
(x - imageBounds.left) / imageBounds.width() to (y - imageBounds.top) / imageBounds.height()
|
|
38
|
+
} to stroke
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun onDraw(canvas: Canvas) {
|
|
43
|
+
strokes.forEach { stroke ->
|
|
44
|
+
if (stroke.points.size < 2) return@forEach
|
|
45
|
+
paint.color = stroke.color; paint.strokeWidth = stroke.widthPx
|
|
46
|
+
val path = Path()
|
|
47
|
+
stroke.points.forEachIndexed { index, point -> if (index == 0) path.moveTo(point.first, point.second) else path.lineTo(point.first, point.second) }
|
|
48
|
+
canvas.drawPath(path, paint)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
53
|
+
if (!imageBounds.contains(event.x, event.y) && event.actionMasked == MotionEvent.ACTION_DOWN) return false
|
|
54
|
+
when (event.actionMasked) {
|
|
55
|
+
MotionEvent.ACTION_DOWN -> {
|
|
56
|
+
current = DrawStroke(mutableListOf(event.x to event.y), strokeColor, strokeWidthPx).also(strokes::add)
|
|
57
|
+
parent?.requestDisallowInterceptTouchEvent(true); invalidate(); return true
|
|
58
|
+
}
|
|
59
|
+
MotionEvent.ACTION_MOVE -> { current?.points?.add(event.x to event.y); invalidate(); return true }
|
|
60
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
61
|
+
current = null; parent?.requestDisallowInterceptTouchEvent(false); onStrokesChanged?.invoke(strokes); return true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -28,6 +28,7 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
28
28
|
var selectedLayerId: String? = null
|
|
29
29
|
set(value) { field = value; invalidate() }
|
|
30
30
|
var onLayerTapped: ((String?) -> Unit)? = null
|
|
31
|
+
var onLayerDoubleTapped: ((String) -> Unit)? = null
|
|
31
32
|
var onLayerTransformChanged: ((id: String, x: Float, y: Float, scale: Float, rotationDegrees: Float) -> Unit)? = null
|
|
32
33
|
var onLayerTransformEnded: ((id: String) -> Unit)? = null
|
|
33
34
|
|
|
@@ -40,6 +41,9 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
40
41
|
private var startAngle = 0f
|
|
41
42
|
private var startMidX = 0f
|
|
42
43
|
private var startMidY = 0f
|
|
44
|
+
private var movedDuringGesture = false
|
|
45
|
+
private var lastTapTimeMs = 0L
|
|
46
|
+
private var lastTapLayerId: String? = null
|
|
43
47
|
private val density = resources.displayMetrics.density
|
|
44
48
|
private val handleRadius = 11f * density
|
|
45
49
|
private val handleTouchRadius = 30f * density
|
|
@@ -79,6 +83,7 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
79
83
|
onLayerTapped?.invoke(hit?.id)
|
|
80
84
|
if (hit == null || hit.locked) { resetGesture(); return false }
|
|
81
85
|
gestureId = hit.id
|
|
86
|
+
movedDuringGesture = false
|
|
82
87
|
startLayer = hit.copy()
|
|
83
88
|
startPointX = event.x; startPointY = event.y
|
|
84
89
|
if (handleHit != null) {
|
|
@@ -103,6 +108,7 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
103
108
|
}
|
|
104
109
|
MotionEvent.ACTION_MOVE -> {
|
|
105
110
|
val initial = startLayer ?: return false
|
|
111
|
+
if (hypot(event.x - startPointX, event.y - startPointY) > handleRadius) movedDuringGesture = true
|
|
106
112
|
when (gesture) {
|
|
107
113
|
Gesture.DRAG -> {
|
|
108
114
|
val x = (initial.x + (event.x - startPointX) / imageBounds.width()).coerceIn(0f, 1f)
|
|
@@ -127,7 +133,16 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
127
133
|
}
|
|
128
134
|
return true
|
|
129
135
|
}
|
|
130
|
-
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
136
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
137
|
+
val tappedId = gestureId
|
|
138
|
+
if (event.actionMasked == MotionEvent.ACTION_UP && !movedDuringGesture && tappedId != null) {
|
|
139
|
+
val now = android.os.SystemClock.uptimeMillis()
|
|
140
|
+
if (lastTapLayerId == tappedId && now - lastTapTimeMs <= 350L) {
|
|
141
|
+
onLayerDoubleTapped?.invoke(tappedId); lastTapLayerId = null; lastTapTimeMs = 0L
|
|
142
|
+
} else { lastTapLayerId = tappedId; lastTapTimeMs = now }
|
|
143
|
+
}
|
|
144
|
+
finishGesture(); return true
|
|
145
|
+
}
|
|
131
146
|
MotionEvent.ACTION_POINTER_UP -> return true
|
|
132
147
|
}
|
|
133
148
|
return false
|
|
@@ -166,6 +181,11 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
166
181
|
val baseSize = shortSide * 0.6f
|
|
167
182
|
if (aspect >= 1f) Pair(baseSize / 2f, (baseSize / aspect) / 2f) else Pair((baseSize * aspect) / 2f, baseSize / 2f)
|
|
168
183
|
}
|
|
184
|
+
LayerType.DRAWING -> {
|
|
185
|
+
val maxX = layer.drawPoints.maxOfOrNull { kotlin.math.abs(it.first) } ?: 0.08f
|
|
186
|
+
val maxY = layer.drawPoints.maxOfOrNull { kotlin.math.abs(it.second) } ?: 0.08f
|
|
187
|
+
Pair(maxX * imageBounds.width() + handleRadius, maxY * imageBounds.height() + handleRadius)
|
|
188
|
+
}
|
|
169
189
|
}
|
|
170
190
|
return Pair((base.first * layer.scale).coerceAtLeast(handleRadius), (base.second * layer.scale).coerceAtLeast(handleRadius))
|
|
171
191
|
}
|
|
@@ -3,12 +3,14 @@ package com.photovideoeditor.stickers
|
|
|
3
3
|
import android.app.Dialog
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import android.graphics.Color
|
|
6
|
+
import android.graphics.BitmapFactory
|
|
6
7
|
import android.os.Bundle
|
|
7
8
|
import android.os.Handler
|
|
8
9
|
import android.os.Looper
|
|
9
10
|
import android.text.Editable
|
|
10
11
|
import android.text.TextWatcher
|
|
11
12
|
import android.view.Gravity
|
|
13
|
+
import android.view.MotionEvent
|
|
12
14
|
import android.view.View
|
|
13
15
|
import android.view.ViewGroup
|
|
14
16
|
import android.view.Window
|
|
@@ -18,7 +20,6 @@ import android.widget.FrameLayout
|
|
|
18
20
|
import android.widget.HorizontalScrollView
|
|
19
21
|
import android.widget.ImageView
|
|
20
22
|
import android.widget.LinearLayout
|
|
21
|
-
import android.widget.ProgressBar
|
|
22
23
|
import android.widget.TextView
|
|
23
24
|
import android.widget.Toast
|
|
24
25
|
import androidx.recyclerview.widget.GridLayoutManager
|
|
@@ -26,41 +27,50 @@ import androidx.recyclerview.widget.RecyclerView
|
|
|
26
27
|
import com.photovideoeditor.ui.DesignTokens
|
|
27
28
|
import com.photovideoeditor.ui.components.dpToPx
|
|
28
29
|
import com.photovideoeditor.ui.components.editorChip
|
|
29
|
-
import com.photovideoeditor.ui.components.editorPrimaryButton
|
|
30
30
|
import com.photovideoeditor.ui.components.roundedDrawable
|
|
31
|
+
import com.photovideoeditor.files.SourceResolver
|
|
32
|
+
import java.io.File
|
|
33
|
+
import java.io.FileOutputStream
|
|
34
|
+
import java.net.URL
|
|
35
|
+
import java.util.concurrent.Executors
|
|
36
|
+
|
|
37
|
+
data class RuntimeSticker(val id: String, val uri: String)
|
|
31
38
|
|
|
32
39
|
/**
|
|
33
|
-
* Bottom sheet listing
|
|
40
|
+
* Bottom sheet listing bundled sticker assets (search + category filter + grid),
|
|
34
41
|
* built as a plain [Dialog] (no Material `BottomSheetDialog`, which isn't a dependency here) that
|
|
35
42
|
* slides up from the bottom and covers ~85% of the screen height.
|
|
36
43
|
*/
|
|
37
44
|
class OnlineStickerSheet(
|
|
38
45
|
context: Context,
|
|
46
|
+
private val runtimeStickers: List<RuntimeSticker> = emptyList(),
|
|
39
47
|
private val onStickerPicked: (localFilePath: String) -> Unit
|
|
40
48
|
) : Dialog(context, android.R.style.Theme_Black_NoTitleBar) {
|
|
41
49
|
|
|
42
50
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
51
|
+
private val ioExecutor = Executors.newFixedThreadPool(3)
|
|
43
52
|
private var searchDebounce: Runnable? = null
|
|
44
53
|
|
|
45
|
-
private
|
|
54
|
+
private val localAssets: List<String> by lazy {
|
|
55
|
+
context.assets.list("Stickers")?.filter { it.endsWith(".png", ignoreCase = true) }?.sorted() ?: emptyList()
|
|
56
|
+
}
|
|
46
57
|
private var selectedGroup: String? = null // null == "All"
|
|
47
58
|
private var searchQuery: String = ""
|
|
48
|
-
private var loadError: Exception? = null
|
|
49
|
-
private var loading = true
|
|
50
59
|
|
|
51
60
|
private lateinit var searchInput: EditText
|
|
52
61
|
private lateinit var chipRow: LinearLayout
|
|
53
62
|
private lateinit var recycler: RecyclerView
|
|
54
|
-
private lateinit var progress: ProgressBar
|
|
55
|
-
private lateinit var statusText: TextView
|
|
56
|
-
private lateinit var retryButton: View
|
|
57
63
|
private lateinit var adapter: StickerAdapter
|
|
58
64
|
|
|
59
65
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
60
66
|
super.onCreate(savedInstanceState)
|
|
61
67
|
setContentView(buildRootView())
|
|
62
68
|
configureWindow()
|
|
63
|
-
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun dismiss() {
|
|
72
|
+
ioExecutor.shutdownNow()
|
|
73
|
+
super.dismiss()
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
private fun configureWindow() {
|
|
@@ -76,7 +86,7 @@ class OnlineStickerSheet(
|
|
|
76
86
|
|
|
77
87
|
private fun buildRootView(): View {
|
|
78
88
|
val screenHeight = context.resources.displayMetrics.heightPixels
|
|
79
|
-
val sheetHeight = (screenHeight * 0.
|
|
89
|
+
val sheetHeight = (screenHeight * 0.78f).toInt()
|
|
80
90
|
|
|
81
91
|
val dimBackground = FrameLayout(context).apply {
|
|
82
92
|
setBackgroundColor(Color.argb(140, 0, 0, 0))
|
|
@@ -90,17 +100,47 @@ class OnlineStickerSheet(
|
|
|
90
100
|
setPadding(dp(DesignTokens.spaceLg), dp(DesignTokens.spaceMd), dp(DesignTokens.spaceLg), dp(DesignTokens.spaceLg))
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
// Functional drag handle with a generous touch target. The sheet follows
|
|
104
|
+
// the finger, dismisses past the threshold, or springs back into place.
|
|
105
|
+
var dragStartY = 0f
|
|
106
|
+
var sheetStartTranslation = 0f
|
|
107
|
+
val dragHandle = FrameLayout(context).apply {
|
|
108
|
+
isClickable = true
|
|
109
|
+
contentDescription = "Drag down to close stickers"
|
|
110
|
+
addView(
|
|
111
|
+
View(context).apply { setBackgroundColor(DesignTokens.outlineVariant) },
|
|
112
|
+
FrameLayout.LayoutParams(dp(40), dp(4), Gravity.CENTER)
|
|
113
|
+
)
|
|
114
|
+
setOnTouchListener { _, event ->
|
|
115
|
+
when (event.actionMasked) {
|
|
116
|
+
MotionEvent.ACTION_DOWN -> {
|
|
117
|
+
dragStartY = event.rawY
|
|
118
|
+
sheetStartTranslation = sheet.translationY
|
|
119
|
+
true
|
|
120
|
+
}
|
|
121
|
+
MotionEvent.ACTION_MOVE -> {
|
|
122
|
+
sheet.translationY = (sheetStartTranslation + event.rawY - dragStartY).coerceAtLeast(0f)
|
|
123
|
+
true
|
|
124
|
+
}
|
|
125
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
126
|
+
val shouldDismiss = sheet.translationY >= minOf(sheet.height * 0.18f, dp(140).toFloat())
|
|
127
|
+
if (shouldDismiss) {
|
|
128
|
+
sheet.animate().translationY(sheet.height.toFloat()).setDuration(180).withEndAction { dismiss() }.start()
|
|
129
|
+
} else {
|
|
130
|
+
sheet.animate().translationY(0f).setDuration(180).start()
|
|
131
|
+
}
|
|
132
|
+
true
|
|
133
|
+
}
|
|
134
|
+
else -> false
|
|
135
|
+
}
|
|
99
136
|
}
|
|
100
|
-
|
|
137
|
+
}
|
|
138
|
+
sheet.addView(dragHandle, LinearLayout.LayoutParams(MATCH, dp(28)).apply {
|
|
139
|
+
bottomMargin = dp(DesignTokens.spaceSm)
|
|
140
|
+
})
|
|
101
141
|
|
|
102
142
|
sheet.addView(TextView(context).apply {
|
|
103
|
-
text = "
|
|
143
|
+
text = "Stickers"
|
|
104
144
|
setTextColor(DesignTokens.textPrimary)
|
|
105
145
|
textSize = 16f
|
|
106
146
|
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
|
@@ -140,43 +180,19 @@ class OnlineStickerSheet(
|
|
|
140
180
|
val gridContainer = FrameLayout(context)
|
|
141
181
|
|
|
142
182
|
recycler = RecyclerView(context).apply {
|
|
143
|
-
layoutManager = GridLayoutManager(context,
|
|
183
|
+
layoutManager = GridLayoutManager(context, 5)
|
|
184
|
+
setHasFixedSize(true)
|
|
144
185
|
}
|
|
145
186
|
adapter = StickerAdapter()
|
|
146
187
|
recycler.adapter = adapter
|
|
147
188
|
gridContainer.addView(recycler, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
148
189
|
|
|
149
|
-
progress = ProgressBar(context)
|
|
150
|
-
gridContainer.addView(progress, FrameLayout.LayoutParams(WRAP, WRAP, Gravity.CENTER))
|
|
151
|
-
|
|
152
|
-
val statusContainer = LinearLayout(context).apply {
|
|
153
|
-
orientation = LinearLayout.VERTICAL
|
|
154
|
-
gravity = Gravity.CENTER
|
|
155
|
-
}
|
|
156
|
-
statusText = TextView(context).apply {
|
|
157
|
-
setTextColor(DesignTokens.textSecondary)
|
|
158
|
-
textSize = 13f
|
|
159
|
-
gravity = Gravity.CENTER
|
|
160
|
-
}
|
|
161
|
-
statusContainer.addView(statusText, LinearLayout.LayoutParams(WRAP, WRAP))
|
|
162
|
-
retryButton = editorPrimaryButton(context, "Retry") { loadCatalog() }
|
|
163
|
-
statusContainer.addView(retryButton, LinearLayout.LayoutParams(WRAP, dp(DesignTokens.touchTargetMin)).apply {
|
|
164
|
-
topMargin = dp(DesignTokens.spaceMd)
|
|
165
|
-
})
|
|
166
|
-
gridContainer.addView(statusContainer, FrameLayout.LayoutParams(WRAP, WRAP, Gravity.CENTER))
|
|
167
|
-
|
|
168
190
|
sheet.addView(gridContainer, LinearLayout.LayoutParams(MATCH, 0, 1f).apply {
|
|
169
191
|
topMargin = dp(DesignTokens.spaceMd)
|
|
170
192
|
})
|
|
171
193
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
setTextColor(DesignTokens.textSubtle)
|
|
175
|
-
textSize = 10f
|
|
176
|
-
gravity = Gravity.CENTER
|
|
177
|
-
}, LinearLayout.LayoutParams(MATCH, WRAP).apply { topMargin = dp(DesignTokens.spaceSm) })
|
|
178
|
-
|
|
179
|
-
updateStatusViews()
|
|
194
|
+
rebuildChips()
|
|
195
|
+
refreshList()
|
|
180
196
|
|
|
181
197
|
val root = FrameLayout(context)
|
|
182
198
|
root.addView(dimBackground, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
@@ -194,27 +210,10 @@ class OnlineStickerSheet(
|
|
|
194
210
|
)
|
|
195
211
|
}
|
|
196
212
|
|
|
197
|
-
private fun loadCatalog() {
|
|
198
|
-
loading = true
|
|
199
|
-
loadError = null
|
|
200
|
-
updateStatusViews()
|
|
201
|
-
OpenMojiCatalog.load(context) { entries, error ->
|
|
202
|
-
loading = false
|
|
203
|
-
if (entries != null) {
|
|
204
|
-
allEntries = entries
|
|
205
|
-
loadError = null
|
|
206
|
-
rebuildChips()
|
|
207
|
-
refreshList()
|
|
208
|
-
} else {
|
|
209
|
-
loadError = error
|
|
210
|
-
}
|
|
211
|
-
updateStatusViews()
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
213
|
private fun rebuildChips() {
|
|
216
214
|
chipRow.removeAllViews()
|
|
217
|
-
val
|
|
215
|
+
val localGroups = localAssets.map { "local:" + it.substringBefore('_').lowercase() }.distinct()
|
|
216
|
+
val groups = listOf<String?>(null) + (if (runtimeStickers.isNotEmpty()) listOf("runtime") else emptyList()) + localGroups
|
|
218
217
|
groups.forEach { group ->
|
|
219
218
|
val label = group?.let { humanizeGroup(it) } ?: "All"
|
|
220
219
|
val chip = editorChip(context, label, selected = group == selectedGroup) {
|
|
@@ -227,32 +226,20 @@ class OnlineStickerSheet(
|
|
|
227
226
|
}
|
|
228
227
|
|
|
229
228
|
private fun humanizeGroup(group: String): String =
|
|
230
|
-
group.split("-").joinToString(" ") { it.replaceFirstChar(Char::uppercase) }
|
|
229
|
+
if (group == "runtime") "My stickers" else group.removePrefix("local:").split("-").joinToString(" ") { it.replaceFirstChar(Char::uppercase) }
|
|
231
230
|
|
|
232
231
|
private fun refreshList() {
|
|
233
232
|
val query = searchQuery.trim()
|
|
234
|
-
val
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
val group =
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
private fun updateStatusViews() {
|
|
245
|
-
progress.visibility = if (loading) View.VISIBLE else View.GONE
|
|
246
|
-
val error = loadError
|
|
247
|
-
val empty = !loading && error == null && adapter.itemCount == 0
|
|
248
|
-
recycler.visibility = if (!loading && error == null && !empty) View.VISIBLE else View.GONE
|
|
249
|
-
statusText.visibility = if (!loading && (error != null || empty)) View.VISIBLE else View.GONE
|
|
250
|
-
retryButton.visibility = if (!loading && error != null) View.VISIBLE else View.GONE
|
|
251
|
-
statusText.text = when {
|
|
252
|
-
error != null -> "Couldn't load stickers — check your connection."
|
|
253
|
-
empty -> "No stickers found."
|
|
254
|
-
else -> ""
|
|
255
|
-
}
|
|
233
|
+
val group = selectedGroup
|
|
234
|
+
val local = localAssets.filter { asset ->
|
|
235
|
+
val matchesQuery = query.isEmpty() || asset.contains(query, ignoreCase = true)
|
|
236
|
+
val matchesGroup = group == null || (group.startsWith("local:") && asset.startsWith(group.removePrefix("local:"), ignoreCase = true))
|
|
237
|
+
matchesQuery && matchesGroup
|
|
238
|
+
}.map { StickerItem(assetName = it) }
|
|
239
|
+
val runtime = runtimeStickers.filter {
|
|
240
|
+
(group == null || group == "runtime") && (query.isEmpty() || it.id.contains(query, ignoreCase = true))
|
|
241
|
+
}.map { StickerItem(runtime = it) }
|
|
242
|
+
adapter.submit(runtime + local)
|
|
256
243
|
}
|
|
257
244
|
|
|
258
245
|
private inner class StickerViewHolder(itemView: FrameLayout) : RecyclerView.ViewHolder(itemView) {
|
|
@@ -260,9 +247,9 @@ class OnlineStickerSheet(
|
|
|
260
247
|
}
|
|
261
248
|
|
|
262
249
|
private inner class StickerAdapter : RecyclerView.Adapter<StickerViewHolder>() {
|
|
263
|
-
private var items: List<
|
|
250
|
+
private var items: List<StickerItem> = emptyList()
|
|
264
251
|
|
|
265
|
-
fun submit(newItems: List<
|
|
252
|
+
fun submit(newItems: List<StickerItem>) {
|
|
266
253
|
items = newItems
|
|
267
254
|
notifyDataSetChanged()
|
|
268
255
|
}
|
|
@@ -270,12 +257,13 @@ class OnlineStickerSheet(
|
|
|
270
257
|
override fun getItemCount(): Int = items.size
|
|
271
258
|
|
|
272
259
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StickerViewHolder {
|
|
273
|
-
val
|
|
260
|
+
val availableWidth = context.resources.displayMetrics.widthPixels - dp(DesignTokens.spaceLg * 2) - dp(8 * 5)
|
|
261
|
+
val cardSize = (availableWidth / 5).coerceAtLeast(dp(52))
|
|
274
262
|
val image = ImageView(context).apply {
|
|
275
263
|
scaleType = ImageView.ScaleType.CENTER_INSIDE
|
|
276
264
|
}
|
|
277
265
|
val cell = FrameLayout(context).apply {
|
|
278
|
-
background = roundedDrawable(DesignTokens.
|
|
266
|
+
background = roundedDrawable(DesignTokens.surfaceContainerLow, DesignTokens.radiusMd, context)
|
|
279
267
|
addView(image, FrameLayout.LayoutParams(cardSize - dp(16), cardSize - dp(16), Gravity.CENTER))
|
|
280
268
|
}
|
|
281
269
|
cell.layoutParams = RecyclerView.LayoutParams(cardSize, cardSize).apply {
|
|
@@ -285,24 +273,67 @@ class OnlineStickerSheet(
|
|
|
285
273
|
}
|
|
286
274
|
|
|
287
275
|
override fun onBindViewHolder(holder: StickerViewHolder, position: Int) {
|
|
288
|
-
val
|
|
289
|
-
|
|
290
|
-
|
|
276
|
+
val item = items[position]
|
|
277
|
+
holder.imageView.setImageDrawable(null)
|
|
278
|
+
item.assetName?.let { asset ->
|
|
279
|
+
holder.imageView.setImageBitmap(context.assets.open("Stickers/$asset").use { BitmapFactory.decodeStream(it) })
|
|
280
|
+
holder.itemView.setOnClickListener { onLocalStickerTapped(asset) }
|
|
281
|
+
}
|
|
282
|
+
item.runtime?.let { runtime ->
|
|
283
|
+
loadRuntimeThumbnail(runtime, holder.imageView)
|
|
284
|
+
holder.itemView.setOnClickListener { onRuntimeStickerTapped(runtime) }
|
|
285
|
+
}
|
|
291
286
|
}
|
|
292
287
|
}
|
|
293
288
|
|
|
294
|
-
private
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
289
|
+
private data class StickerItem(val assetName: String? = null, val runtime: RuntimeSticker? = null)
|
|
290
|
+
|
|
291
|
+
private fun loadRuntimeThumbnail(sticker: RuntimeSticker, imageView: ImageView) {
|
|
292
|
+
imageView.tag = sticker.uri
|
|
293
|
+
ioExecutor.execute {
|
|
294
|
+
val bitmap = try {
|
|
295
|
+
if (sticker.uri.startsWith("https://")) downloadRuntimeSticker(sticker.uri).inputStream().use(BitmapFactory::decodeStream)
|
|
296
|
+
else SourceResolver.resolvePath(context, sticker.uri, "pve_runtime_sticker")?.let(BitmapFactory::decodeFile)
|
|
297
|
+
} catch (_: Exception) { null }
|
|
298
|
+
mainHandler.post { if (imageView.tag == sticker.uri) imageView.setImageBitmap(bitmap) }
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private fun onRuntimeStickerTapped(sticker: RuntimeSticker) {
|
|
303
|
+
ioExecutor.execute {
|
|
304
|
+
val path = try {
|
|
305
|
+
if (sticker.uri.startsWith("https://")) {
|
|
306
|
+
val output = File.createTempFile("pve_runtime_sticker_", ".img", context.cacheDir)
|
|
307
|
+
output.writeBytes(downloadRuntimeSticker(sticker.uri))
|
|
308
|
+
output.absolutePath
|
|
309
|
+
} else SourceResolver.resolvePath(context, sticker.uri, "pve_runtime_sticker")
|
|
310
|
+
} catch (_: Exception) { null }
|
|
311
|
+
mainHandler.post {
|
|
312
|
+
if (path != null) { onStickerPicked(path); dismiss() }
|
|
313
|
+
else Toast.makeText(context, "Couldn't open that sticker.", Toast.LENGTH_SHORT).show()
|
|
302
314
|
}
|
|
303
315
|
}
|
|
304
316
|
}
|
|
305
317
|
|
|
318
|
+
private fun downloadRuntimeSticker(uri: String): ByteArray {
|
|
319
|
+
val connection = URL(uri).openConnection().apply {
|
|
320
|
+
connectTimeout = 10_000
|
|
321
|
+
readTimeout = 15_000
|
|
322
|
+
}
|
|
323
|
+
return connection.getInputStream().use { it.readBytes() }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private fun onLocalStickerTapped(assetName: String) {
|
|
327
|
+
try {
|
|
328
|
+
val output = File(context.cacheDir, "sticker-${assetName.lowercase()}")
|
|
329
|
+
context.assets.open("Stickers/$assetName").use { input -> FileOutputStream(output).use(input::copyTo) }
|
|
330
|
+
onStickerPicked(output.absolutePath)
|
|
331
|
+
dismiss()
|
|
332
|
+
} catch (_: Exception) {
|
|
333
|
+
Toast.makeText(context, "Couldn't open that sticker.", Toast.LENGTH_SHORT).show()
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
306
337
|
companion object {
|
|
307
338
|
private const val MATCH = ViewGroup.LayoutParams.MATCH_PARENT
|
|
308
339
|
private const val WRAP = ViewGroup.LayoutParams.WRAP_CONTENT
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960">
|
|
3
|
+
<group android:translateY="960">
|
|
4
|
+
<path android:fillColor="#FFFFFF" android:pathData="M160-120v-170l527-526q12-12 27-18t30-6q16 0 30.5 6t25.5 18l56 56q12 11 18 25.5t6 30.5q0 15-6 30t-18 27L330-120H160Zm80-80h56l393-392-28-29-29-28-392 393v56Zm320 80q74 0 137-37t63-103q0-36-19-62t-51-45l-59 59q23 10 36 22t13 26q0 23-36.5 41.5T560-200q-17 0-28.5 11.5T520-160q0 17 11.5 28.5T560-120ZM183-426l60-60q-20-8-31.5-16.5T200-520q0-12 18-24t76-37q88-38 117-69t29-70q0-55-44-87.5T280-840q-45 0-80.5 16T145-785q-11 13-9 29t15 26q13 11 29 9t27-13q14-14 31-20t42-6q41 0 60.5 12t19.5 28q0 14-17.5 25.5T262-654q-80 35-111 63.5T120-520q0 32 17 54.5t46 39.5Z"/>
|
|
5
|
+
</group>
|
|
6
|
+
</vector>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
android:width="24dp"
|
|
3
|
+
android:height="24dp"
|
|
4
|
+
android:viewportWidth="24"
|
|
5
|
+
android:viewportHeight="24">
|
|
6
|
+
<path android:fillColor="#FFFFFFFF" android:pathData="M6,19h4V5H6v14zM14,5v14h4V5h-4z" />
|
|
7
|
+
</vector>
|
package/docs/api.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`openEditor(options)` validates defaults, invokes native code, and returns `Promise<EditorResult>`. In milestone one it returns the source URI/type and `cancelled: false`; it does not present UI or export. `cancelExport(jobId?)` currently returns `false`. `isAvailable()` synchronously checks native linkage. `addExportProgressListener(listener)` returns `{ remove() }`; no fake events are emitted.
|
|
4
4
|
|
|
5
|
-
Defaults enable all feature flags
|
|
5
|
+
Defaults enable all feature flags—including `text`, `stickers`, `overlays`, and `draw`—set quality to `high`, preserve metadata, and disable gallery saving. For photos, `trim` normalizes to `false`. Photo/video format mismatches and unknown enum values are rejected.
|
|
6
6
|
|
|
7
7
|
Dimensions use pixels, duration milliseconds, file size bytes, bitrate bits/second, frame rate frames/second, and progress ranges from 0 to 1. Public types and option-level JSDoc live in `src/types.ts`.
|
|
8
8
|
|
package/docs/photo-editor.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Milestone 2 — transform and export
|
|
4
4
|
|
|
5
|
-
- **Crop** —
|
|
5
|
+
- **Crop** — dedicated ZLImageEditor-inspired crop mode with a dimmed mask, rule-of-thirds grid, four corner and four edge handles, movable crop frame, and aspect presets (Free, 1:1, 4:5, 3:4, 9:16, 16:9). **Cancel** restores the exact pre-crop state; **Done** commits the crop.
|
|
6
6
|
- **Rotate** — rotates 90° per tap. Resets the current crop (a rotated image has a different frame of reference for the crop rectangle).
|
|
7
7
|
- **Flip** — cycles none → horizontal → vertical → none.
|
|
8
8
|
- **Straighten** — a fine -45°...45° slider, shown while crop mode is active.
|
|
@@ -18,13 +18,13 @@ Tap **Filters** to enter adjustment mode: a horizontal chip row selects which co
|
|
|
18
18
|
- **Reset** (filters sub-bar) — restores all adjustments to their zero/no-op defaults, independent of crop/transform/layers.
|
|
19
19
|
- Same pipeline and formulas applied in preview and export on both platforms.
|
|
20
20
|
|
|
21
|
-
## Milestone 4 — layers: text, stickers,
|
|
21
|
+
## Milestone 4 — layers: text, stickers, images, drawing
|
|
22
22
|
|
|
23
23
|
- **Text** — tap **Text**, type a string in the dialog, and it's added as a centered, draggable text layer.
|
|
24
|
-
- **Stickers** — tap **Stickers** for
|
|
24
|
+
- **Stickers** — tap **Stickers** for the searchable, categorized sticker collection bundled with the package. No third-party sticker API or network connection is used.
|
|
25
25
|
- **Draw** — tap **Draw** for a freehand canvas: pick a color and thin/thick width, draw multiple strokes, undo the last stroke or clear all, then **Done** commits every stroke as its own layer (so strokes with different colors/widths keep their own settings).
|
|
26
26
|
- **Selection** — tap any layer on the canvas to select it (shown with a dashed bounding box) and open its mini toolbar.
|
|
27
|
-
- **Transform** —
|
|
27
|
+
- **Transform** — drag layers to move them, pinch with two fingers to scale and rotate, or use the corner handle. Scale/Rotate/Opacity sliders remain available for precise changes.
|
|
28
28
|
- **Duplicate / Lock / Hide / Front / Back / Delete** — per-layer actions in the mini toolbar.
|
|
29
29
|
- **Undo/redo** — header buttons, backed by a snapshot history (50 entries). One entry is recorded per *committed* operation — add/delete/duplicate/reorder, or a drag/slider gesture's final value — never per intermediate touch-move frame.
|
|
30
30
|
- Layers are burned into the image identically in preview and at export, positioned in the same full-frame coordinate space as the crop overlay (so cropping after adding layers can crop them out, matching what the crop preview would suggest).
|
|
@@ -32,8 +32,7 @@ Tap **Filters** to enter adjustment mode: a horizontal chip row selects which co
|
|
|
32
32
|
### Known gaps in Milestone 4
|
|
33
33
|
|
|
34
34
|
- **Font registration is not wired to custom font files yet.** `EditorOptions.fonts` is accepted structurally (passed through to the native layer) but text layers currently only resolve `fontFamily` against system-installed font names (Android `Typeface.create`, iOS `UIFont(name:)`) — a consumer-provided font *file* is not loaded. This is an honest scope cut, not a silent gap: closing it means adding a font-file loader (`Typeface.createFromFile` / `CTFontManagerRegisterFontsForURL`) on each platform, planned as a small follow-up.
|
|
35
|
-
-
|
|
36
|
-
- No sealed per-type layer model — one flat `PhotoLayer` struct/data class covers text/sticker/shape/drawing, a deliberate simplification.
|
|
35
|
+
- No sealed per-type layer model — one flat `PhotoLayer` struct/data class covers text/sticker/image/drawing, a deliberate simplification.
|
|
37
36
|
- Text styling is limited to color and font size for now (no stroke/background/shadow/alignment controls yet, though the fields exist on `PhotoLayer` for a later pass).
|
|
38
37
|
|
|
39
38
|
Export applies the combined transform + adjustments + layers to the full-resolution source (never mutated) and writes JPEG or PNG (WebP on Android only; iOS falls back to JPEG). Output width/height/fileSize/mimeType are returned on the `EditorResult`.
|