react-native-photo-video-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.
Files changed (233) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/PhotoVideoEditor.podspec +1 -0
  3. package/README.md +10 -6
  4. package/THIRD_PARTY_NOTICES.md +20 -0
  5. package/android/build.gradle +1 -1
  6. package/android/src/main/AndroidManifest.xml +1 -1
  7. package/android/src/main/assets/Stickers/AUTUMN_1.png +0 -0
  8. package/android/src/main/assets/Stickers/AUTUMN_10.png +0 -0
  9. package/android/src/main/assets/Stickers/AUTUMN_11.png +0 -0
  10. package/android/src/main/assets/Stickers/AUTUMN_12.png +0 -0
  11. package/android/src/main/assets/Stickers/AUTUMN_13.png +0 -0
  12. package/android/src/main/assets/Stickers/AUTUMN_14.png +0 -0
  13. package/android/src/main/assets/Stickers/AUTUMN_15.png +0 -0
  14. package/android/src/main/assets/Stickers/AUTUMN_16.png +0 -0
  15. package/android/src/main/assets/Stickers/AUTUMN_17.png +0 -0
  16. package/android/src/main/assets/Stickers/AUTUMN_18.png +0 -0
  17. package/android/src/main/assets/Stickers/AUTUMN_2.png +0 -0
  18. package/android/src/main/assets/Stickers/AUTUMN_3.png +0 -0
  19. package/android/src/main/assets/Stickers/AUTUMN_4.png +0 -0
  20. package/android/src/main/assets/Stickers/AUTUMN_5.png +0 -0
  21. package/android/src/main/assets/Stickers/AUTUMN_6.png +0 -0
  22. package/android/src/main/assets/Stickers/AUTUMN_7.png +0 -0
  23. package/android/src/main/assets/Stickers/AUTUMN_8.png +0 -0
  24. package/android/src/main/assets/Stickers/AUTUMN_9.png +0 -0
  25. package/android/src/main/assets/Stickers/LOL_1.png +0 -0
  26. package/android/src/main/assets/Stickers/LOL_10.png +0 -0
  27. package/android/src/main/assets/Stickers/LOL_11.png +0 -0
  28. package/android/src/main/assets/Stickers/LOL_12.png +0 -0
  29. package/android/src/main/assets/Stickers/LOL_13.png +0 -0
  30. package/android/src/main/assets/Stickers/LOL_14.png +0 -0
  31. package/android/src/main/assets/Stickers/LOL_15.png +0 -0
  32. package/android/src/main/assets/Stickers/LOL_16.png +0 -0
  33. package/android/src/main/assets/Stickers/LOL_17.png +0 -0
  34. package/android/src/main/assets/Stickers/LOL_18.png +0 -0
  35. package/android/src/main/assets/Stickers/LOL_19.png +0 -0
  36. package/android/src/main/assets/Stickers/LOL_2.png +0 -0
  37. package/android/src/main/assets/Stickers/LOL_3.png +0 -0
  38. package/android/src/main/assets/Stickers/LOL_4.png +0 -0
  39. package/android/src/main/assets/Stickers/LOL_5.png +0 -0
  40. package/android/src/main/assets/Stickers/LOL_6.png +0 -0
  41. package/android/src/main/assets/Stickers/LOL_7.png +0 -0
  42. package/android/src/main/assets/Stickers/LOL_8.png +0 -0
  43. package/android/src/main/assets/Stickers/LOL_9.png +0 -0
  44. package/android/src/main/assets/Stickers/SPRING_1.png +0 -0
  45. package/android/src/main/assets/Stickers/SPRING_2.png +0 -0
  46. package/android/src/main/assets/Stickers/SPRING_3.png +0 -0
  47. package/android/src/main/assets/Stickers/SPRING_4.png +0 -0
  48. package/android/src/main/assets/Stickers/SPRING_5.png +0 -0
  49. package/android/src/main/assets/Stickers/SPRING_6.png +0 -0
  50. package/android/src/main/assets/Stickers/SUMMER_1.png +0 -0
  51. package/android/src/main/assets/Stickers/SUMMER_10.png +0 -0
  52. package/android/src/main/assets/Stickers/SUMMER_11.png +0 -0
  53. package/android/src/main/assets/Stickers/SUMMER_12.png +0 -0
  54. package/android/src/main/assets/Stickers/SUMMER_13.png +0 -0
  55. package/android/src/main/assets/Stickers/SUMMER_14.png +0 -0
  56. package/android/src/main/assets/Stickers/SUMMER_15.png +0 -0
  57. package/android/src/main/assets/Stickers/SUMMER_16.png +0 -0
  58. package/android/src/main/assets/Stickers/SUMMER_17.png +0 -0
  59. package/android/src/main/assets/Stickers/SUMMER_18.png +0 -0
  60. package/android/src/main/assets/Stickers/SUMMER_19.png +0 -0
  61. package/android/src/main/assets/Stickers/SUMMER_2.png +0 -0
  62. package/android/src/main/assets/Stickers/SUMMER_20.png +0 -0
  63. package/android/src/main/assets/Stickers/SUMMER_21.png +0 -0
  64. package/android/src/main/assets/Stickers/SUMMER_3.png +0 -0
  65. package/android/src/main/assets/Stickers/SUMMER_4.png +0 -0
  66. package/android/src/main/assets/Stickers/SUMMER_5.png +0 -0
  67. package/android/src/main/assets/Stickers/SUMMER_6.png +0 -0
  68. package/android/src/main/assets/Stickers/SUMMER_7.png +0 -0
  69. package/android/src/main/assets/Stickers/SUMMER_8.png +0 -0
  70. package/android/src/main/assets/Stickers/SUMMER_9.png +0 -0
  71. package/android/src/main/assets/Stickers/TEXT_1.png +0 -0
  72. package/android/src/main/assets/Stickers/TEXT_12.png +0 -0
  73. package/android/src/main/assets/Stickers/TEXT_13.png +0 -0
  74. package/android/src/main/assets/Stickers/TEXT_14.png +0 -0
  75. package/android/src/main/assets/Stickers/TEXT_15.png +0 -0
  76. package/android/src/main/assets/Stickers/TEXT_16.png +0 -0
  77. package/android/src/main/assets/Stickers/TEXT_2.png +0 -0
  78. package/android/src/main/assets/Stickers/TEXT_25.png +0 -0
  79. package/android/src/main/assets/Stickers/TEXT_26.png +0 -0
  80. package/android/src/main/assets/Stickers/TEXT_27.png +0 -0
  81. package/android/src/main/assets/Stickers/TEXT_28.png +0 -0
  82. package/android/src/main/assets/Stickers/TEXT_29.png +0 -0
  83. package/android/src/main/assets/Stickers/TEXT_3.png +0 -0
  84. package/android/src/main/assets/Stickers/TEXT_30.png +0 -0
  85. package/android/src/main/assets/Stickers/TEXT_31.png +0 -0
  86. package/android/src/main/assets/Stickers/TEXT_32.png +0 -0
  87. package/android/src/main/assets/Stickers/TEXT_7.png +0 -0
  88. package/android/src/main/assets/Stickers/TEXT_8.png +0 -0
  89. package/android/src/main/assets/Stickers/TEXT_9.png +0 -0
  90. package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +901 -651
  91. package/android/src/main/java/com/photovideoeditor/photo/export/PhotoExporter.kt +8 -23
  92. package/android/src/main/java/com/photovideoeditor/photo/render/CropGeometry.kt +71 -0
  93. package/android/src/main/java/com/photovideoeditor/photo/render/OverlayGeometry.kt +36 -0
  94. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoEditSession.kt +5 -7
  95. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoFilterPresets.kt +71 -41
  96. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayer.kt +5 -1
  97. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +24 -1
  98. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformRenderer.kt +33 -0
  99. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformState.kt +6 -1
  100. package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +112 -122
  101. package/android/src/main/java/com/photovideoeditor/photo/ui/DrawOverlayView.kt +66 -0
  102. package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +63 -14
  103. package/android/src/main/java/com/photovideoeditor/photo/ui/ZoomableImageView.kt +10 -0
  104. package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +135 -104
  105. package/android/src/main/java/com/photovideoeditor/ui/components/CropPanel.kt +122 -0
  106. package/android/src/main/java/com/photovideoeditor/ui/components/EditorPanel.kt +250 -0
  107. package/android/src/main/java/com/photovideoeditor/ui/components/EditorToolSlider.kt +103 -0
  108. package/android/src/main/java/com/photovideoeditor/video/export/VideoExporter.kt +38 -11
  109. package/android/src/main/java/com/photovideoeditor/video/render/VideoCropEffects.kt +35 -0
  110. package/android/src/main/java/com/photovideoeditor/video/render/VideoEditSession.kt +10 -0
  111. package/android/src/main/java/com/photovideoeditor/video/render/VideoTransformState.kt +1 -0
  112. package/android/src/main/res/drawable/ic_draw.xml +6 -0
  113. package/android/src/main/res/drawable/ic_pause.xml +7 -0
  114. package/android/src/main/res/drawable/ic_play.xml +7 -0
  115. package/docs/api.md +1 -1
  116. package/docs/editor-ui-validation.md +22 -0
  117. package/docs/photo-editor.md +5 -6
  118. package/ios/Editor/PhotoVideoEditorViewController.swift +918 -494
  119. package/ios/Editor/UI/CropPanel.swift +97 -0
  120. package/ios/Editor/UI/EditorPanel.swift +334 -0
  121. package/ios/Editor/UI/EditorToolSlider.swift +22 -0
  122. package/ios/Editor/UI/TextEditorSheet.swift +84 -0
  123. package/ios/Photo/Export/PhotoExporter.swift +3 -3
  124. package/ios/Photo/Rendering/CropGeometry.swift +48 -0
  125. package/ios/Photo/Rendering/PhotoEditSession.swift +21 -10
  126. package/ios/Photo/Rendering/PhotoFilterPresets.swift +53 -23
  127. package/ios/Photo/Rendering/PhotoLayer.swift +14 -2
  128. package/ios/Photo/Rendering/PhotoLayerRenderer.swift +16 -1
  129. package/ios/Photo/Rendering/PhotoTransformState.swift +5 -0
  130. package/ios/Photo/UI/CropOverlayView.swift +98 -134
  131. package/ios/Photo/UI/DrawOverlayView.swift +48 -0
  132. package/ios/Photo/UI/FilterThumbnailLoader.swift +62 -0
  133. package/ios/Photo/UI/LayerOverlayView.swift +36 -11
  134. package/ios/Photo/UI/ZoomableImageView.swift +19 -1
  135. package/ios/Resources/Stickers/AUTUMN_1.png +0 -0
  136. package/ios/Resources/Stickers/AUTUMN_10.png +0 -0
  137. package/ios/Resources/Stickers/AUTUMN_11.png +0 -0
  138. package/ios/Resources/Stickers/AUTUMN_12.png +0 -0
  139. package/ios/Resources/Stickers/AUTUMN_13.png +0 -0
  140. package/ios/Resources/Stickers/AUTUMN_14.png +0 -0
  141. package/ios/Resources/Stickers/AUTUMN_15.png +0 -0
  142. package/ios/Resources/Stickers/AUTUMN_16.png +0 -0
  143. package/ios/Resources/Stickers/AUTUMN_17.png +0 -0
  144. package/ios/Resources/Stickers/AUTUMN_18.png +0 -0
  145. package/ios/Resources/Stickers/AUTUMN_2.png +0 -0
  146. package/ios/Resources/Stickers/AUTUMN_3.png +0 -0
  147. package/ios/Resources/Stickers/AUTUMN_4.png +0 -0
  148. package/ios/Resources/Stickers/AUTUMN_5.png +0 -0
  149. package/ios/Resources/Stickers/AUTUMN_6.png +0 -0
  150. package/ios/Resources/Stickers/AUTUMN_7.png +0 -0
  151. package/ios/Resources/Stickers/AUTUMN_8.png +0 -0
  152. package/ios/Resources/Stickers/AUTUMN_9.png +0 -0
  153. package/ios/Resources/Stickers/LOL_1.png +0 -0
  154. package/ios/Resources/Stickers/LOL_10.png +0 -0
  155. package/ios/Resources/Stickers/LOL_11.png +0 -0
  156. package/ios/Resources/Stickers/LOL_12.png +0 -0
  157. package/ios/Resources/Stickers/LOL_13.png +0 -0
  158. package/ios/Resources/Stickers/LOL_14.png +0 -0
  159. package/ios/Resources/Stickers/LOL_15.png +0 -0
  160. package/ios/Resources/Stickers/LOL_16.png +0 -0
  161. package/ios/Resources/Stickers/LOL_17.png +0 -0
  162. package/ios/Resources/Stickers/LOL_18.png +0 -0
  163. package/ios/Resources/Stickers/LOL_19.png +0 -0
  164. package/ios/Resources/Stickers/LOL_2.png +0 -0
  165. package/ios/Resources/Stickers/LOL_3.png +0 -0
  166. package/ios/Resources/Stickers/LOL_4.png +0 -0
  167. package/ios/Resources/Stickers/LOL_5.png +0 -0
  168. package/ios/Resources/Stickers/LOL_6.png +0 -0
  169. package/ios/Resources/Stickers/LOL_7.png +0 -0
  170. package/ios/Resources/Stickers/LOL_8.png +0 -0
  171. package/ios/Resources/Stickers/LOL_9.png +0 -0
  172. package/ios/Resources/Stickers/SPRING_1.png +0 -0
  173. package/ios/Resources/Stickers/SPRING_2.png +0 -0
  174. package/ios/Resources/Stickers/SPRING_3.png +0 -0
  175. package/ios/Resources/Stickers/SPRING_4.png +0 -0
  176. package/ios/Resources/Stickers/SPRING_5.png +0 -0
  177. package/ios/Resources/Stickers/SPRING_6.png +0 -0
  178. package/ios/Resources/Stickers/SUMMER_1.png +0 -0
  179. package/ios/Resources/Stickers/SUMMER_10.png +0 -0
  180. package/ios/Resources/Stickers/SUMMER_11.png +0 -0
  181. package/ios/Resources/Stickers/SUMMER_12.png +0 -0
  182. package/ios/Resources/Stickers/SUMMER_13.png +0 -0
  183. package/ios/Resources/Stickers/SUMMER_14.png +0 -0
  184. package/ios/Resources/Stickers/SUMMER_15.png +0 -0
  185. package/ios/Resources/Stickers/SUMMER_16.png +0 -0
  186. package/ios/Resources/Stickers/SUMMER_17.png +0 -0
  187. package/ios/Resources/Stickers/SUMMER_18.png +0 -0
  188. package/ios/Resources/Stickers/SUMMER_19.png +0 -0
  189. package/ios/Resources/Stickers/SUMMER_2.png +0 -0
  190. package/ios/Resources/Stickers/SUMMER_20.png +0 -0
  191. package/ios/Resources/Stickers/SUMMER_21.png +0 -0
  192. package/ios/Resources/Stickers/SUMMER_3.png +0 -0
  193. package/ios/Resources/Stickers/SUMMER_4.png +0 -0
  194. package/ios/Resources/Stickers/SUMMER_5.png +0 -0
  195. package/ios/Resources/Stickers/SUMMER_6.png +0 -0
  196. package/ios/Resources/Stickers/SUMMER_7.png +0 -0
  197. package/ios/Resources/Stickers/SUMMER_8.png +0 -0
  198. package/ios/Resources/Stickers/SUMMER_9.png +0 -0
  199. package/ios/Resources/Stickers/TEXT_1.png +0 -0
  200. package/ios/Resources/Stickers/TEXT_12.png +0 -0
  201. package/ios/Resources/Stickers/TEXT_13.png +0 -0
  202. package/ios/Resources/Stickers/TEXT_14.png +0 -0
  203. package/ios/Resources/Stickers/TEXT_15.png +0 -0
  204. package/ios/Resources/Stickers/TEXT_16.png +0 -0
  205. package/ios/Resources/Stickers/TEXT_2.png +0 -0
  206. package/ios/Resources/Stickers/TEXT_25.png +0 -0
  207. package/ios/Resources/Stickers/TEXT_26.png +0 -0
  208. package/ios/Resources/Stickers/TEXT_27.png +0 -0
  209. package/ios/Resources/Stickers/TEXT_28.png +0 -0
  210. package/ios/Resources/Stickers/TEXT_29.png +0 -0
  211. package/ios/Resources/Stickers/TEXT_3.png +0 -0
  212. package/ios/Resources/Stickers/TEXT_30.png +0 -0
  213. package/ios/Resources/Stickers/TEXT_31.png +0 -0
  214. package/ios/Resources/Stickers/TEXT_32.png +0 -0
  215. package/ios/Resources/Stickers/TEXT_7.png +0 -0
  216. package/ios/Resources/Stickers/TEXT_8.png +0 -0
  217. package/ios/Resources/Stickers/TEXT_9.png +0 -0
  218. package/ios/Resources/Stickers/a_pety_icon.png +0 -0
  219. package/ios/Stickers/OnlineStickerSheetViewController.swift +102 -159
  220. package/ios/Video/Export/VideoExporter.swift +10 -1
  221. package/ios/Video/Render/VideoCropGeometry.swift +48 -0
  222. package/ios/Video/Render/VideoTransformState.swift +1 -0
  223. package/lib/module/index.js +2 -2
  224. package/lib/module/index.js.map +1 -1
  225. package/lib/typescript/src/types.d.ts +2 -2
  226. package/lib/typescript/src/types.d.ts.map +1 -1
  227. package/package.json +2 -1
  228. package/src/index.ts +1 -1
  229. package/src/types.ts +2 -2
  230. package/android/src/main/java/com/photovideoeditor/stickers/OpenMojiCatalog.kt +0 -172
  231. package/android/src/main/java/com/photovideoeditor/stickers/RemoteImageLoader.kt +0 -110
  232. package/ios/Stickers/OpenMojiCatalog.swift +0 -188
  233. package/ios/Stickers/RemoteImageLoader.swift +0 -127
@@ -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 OpenMoji's free sticker catalog (search + category filter + grid),
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 var allEntries: List<OpenMojiEntry> = emptyList()
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
- loadCatalog()
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.85f).toInt()
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
- // Decorative drag handle.
94
- sheet.addView(
95
- View(context).apply { setBackgroundColor(DesignTokens.outlineVariant) },
96
- LinearLayout.LayoutParams(dp(36), dp(4)).apply {
97
- gravity = Gravity.CENTER_HORIZONTAL
98
- bottomMargin = dp(DesignTokens.spaceMd)
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 = "Browse stickers"
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, 4)
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
- sheet.addView(TextView(context).apply {
173
- text = "Stickers by OpenMoji (CC BY-SA 4.0)"
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 groups = listOf<String?>(null) + OpenMojiCatalog.groups()
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 list = if (query.isNotEmpty()) {
235
- OpenMojiCatalog.search(query)
236
- } else {
237
- val group = selectedGroup
238
- if (group == null) allEntries else OpenMojiCatalog.byGroup(group)
239
- }
240
- adapter.submit(list)
241
- updateStatusViews()
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<OpenMojiEntry> = emptyList()
250
+ private var items: List<StickerItem> = emptyList()
264
251
 
265
- fun submit(newItems: List<OpenMojiEntry>) {
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 cardSize = dp(72)
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.surfaceContainer, DesignTokens.radiusMd, context)
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 entry = items[position]
289
- RemoteImageLoader.loadThumbnail(context, OpenMojiCatalog.thumbnailUrl(entry.hexcode), holder.imageView)
290
- holder.itemView.setOnClickListener { onStickerTapped(entry) }
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 fun onStickerTapped(entry: OpenMojiEntry) {
295
- Toast.makeText(context, "Downloading sticker…", Toast.LENGTH_SHORT).show()
296
- RemoteImageLoader.downloadFull(context, OpenMojiCatalog.fullUrl(entry.hexcode)) { path, error ->
297
- if (path != null) {
298
- onStickerPicked(path)
299
- dismiss()
300
- } else {
301
- Toast.makeText(context, "Couldn't download that sticker, try another.", Toast.LENGTH_SHORT).show()
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,122 @@
1
+ package com.photovideoeditor.ui.components
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.drawable.GradientDrawable
8
+ import android.view.Gravity
9
+ import android.widget.*
10
+ import com.photovideoeditor.R
11
+ import com.photovideoeditor.photo.render.PhotoTransformState
12
+ import com.photovideoeditor.ui.DesignTokens
13
+ import kotlin.math.roundToInt
14
+
15
+ /** Shared photo/video crop surface. Only UI state lives here; the controller owns the draft. */
16
+ class CropPanel(
17
+ context: Context,
18
+ private val accent: Int,
19
+ surface: Int,
20
+ textColor: Int,
21
+ originalRatio: () -> Float,
22
+ onReset: () -> Unit,
23
+ onDone: () -> Unit,
24
+ onCancel: () -> Unit,
25
+ onStraighten: (Float) -> Unit,
26
+ onRotate: () -> Unit,
27
+ onRatio: (String, Float?) -> Unit
28
+ ) : LinearLayout(context) {
29
+ private fun dp(value: Int) = dpToPx(value, context)
30
+ private val valueLabel = TextView(context)
31
+ private val slider: SeekBar
32
+ private val presets = mutableMapOf<String, LinearLayout>()
33
+ private var syncing = false
34
+
35
+ init {
36
+ orientation = VERTICAL
37
+ setPadding(dp(12), dp(8), dp(12), dp(8))
38
+ background = GradientDrawable().apply {
39
+ setColor(surface); cornerRadii = floatArrayOf(dp(20).toFloat(), dp(20).toFloat(), dp(20).toFloat(), dp(20).toFloat(), 0f, 0f, 0f, 0f)
40
+ }
41
+ fun icon(resource: Int, label: String, action: () -> Unit) = ImageButton(context).apply {
42
+ setImageResource(resource); setColorFilter(textColor); contentDescription = label
43
+ background = roundedDrawable(Color.TRANSPARENT, 12, context)
44
+ setOnClickListener { action() }; applyPressScale()
45
+ }
46
+ val header = LinearLayout(context).apply { gravity = Gravity.CENTER_VERTICAL }
47
+ val reset = LinearLayout(context).apply {
48
+ gravity = Gravity.CENTER_VERTICAL; isClickable = true; contentDescription = "Reset crop"
49
+ addView(ImageView(context).apply { setImageResource(R.drawable.ic_undo); setColorFilter(textColor) }, LayoutParams(dp(20), dp(20)))
50
+ addView(TextView(context).apply { text = "Reset"; textSize = 13f; setTextColor(textColor); setPadding(dp(6), 0, 0, 0) })
51
+ setOnClickListener { onReset() }; applyPressScale()
52
+ }
53
+ header.addView(reset, LayoutParams(dp(88), dp(48)))
54
+ header.addView(TextView(context).apply { text = "Crop"; textSize = 17f; gravity = Gravity.CENTER; setTextColor(textColor); setTypeface(typeface, 1) }, LayoutParams(0, dp(48), 1f))
55
+ header.addView(icon(R.drawable.ic_close, "Cancel crop", onCancel), LayoutParams(dp(44), dp(48)))
56
+ header.addView(icon(R.drawable.ic_check, "Apply crop", onDone).apply { setColorFilter(accent) }, LayoutParams(dp(44), dp(48)))
57
+ addView(header)
58
+ val labels = LinearLayout(context).apply { gravity = Gravity.CENTER_VERTICAL }
59
+ labels.addView(TextView(context).apply { text = "Straighten"; textSize = 13f; setTextColor(textColor) }, LayoutParams(0, dp(44), 1f))
60
+ valueLabel.setTextColor(accent); valueLabel.textSize = 13f; valueLabel.gravity = Gravity.CENTER
61
+ labels.addView(valueLabel, LayoutParams(dp(56), dp(44)))
62
+ labels.addView(icon(R.drawable.ic_undo, "Reset straighten") { onStraighten(0f) }, LayoutParams(dp(44), dp(44)))
63
+ labels.addView(icon(R.drawable.ic_rotate_right, "Rotate 90 degrees", onRotate), LayoutParams(dp(44), dp(44)))
64
+ addView(labels)
65
+ slider = object : SeekBar(context) {
66
+ private val tick = Paint(Paint.ANTI_ALIAS_FLAG).apply { strokeWidth = resources.displayMetrics.density; color = DesignTokens.outline }
67
+ override fun onDraw(canvas: Canvas) {
68
+ val start = paddingLeft.toFloat(); val end = width - paddingRight.toFloat()
69
+ for (i in 0..90) {
70
+ val x = start + (end - start) * i / 90
71
+ tick.alpha = if (i % 5 == 0) 160 else 65
72
+ canvas.drawLine(x, 6f, x, if (i % 5 == 0) dp(12).toFloat() else dp(7).toFloat(), tick)
73
+ }
74
+ super.onDraw(canvas)
75
+ }
76
+ }.apply {
77
+ max = 900; progress = 450; contentDescription = "Straighten, minus 45 to plus 45 degrees"
78
+ progressTintList = android.content.res.ColorStateList.valueOf(accent); thumbTintList = progressTintList
79
+ setPadding(dp(12), dp(12), dp(12), 0)
80
+ setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
81
+ override fun onProgressChanged(bar: SeekBar?, progress: Int, fromUser: Boolean) { if (fromUser && !syncing) onStraighten((progress - 450) / 10f) }
82
+ override fun onStartTrackingTouch(bar: SeekBar?) {}
83
+ override fun onStopTrackingTouch(bar: SeekBar?) {}
84
+ })
85
+ }
86
+ addView(slider, LayoutParams(LayoutParams.MATCH_PARENT, dp(48)))
87
+ val rail = LinearLayout(context).apply { gravity = Gravity.CENTER_VERTICAL }
88
+ val ratios = listOf("Free" to null, "Original" to null, "1:1" to 1f, "9:16" to 9f / 16, "16:9" to 16f / 9, "10:16" to 10f / 16, "16:10" to 16f / 10, "4:5" to 4f / 5, "5:4" to 5f / 4, "3:4" to 3f / 4, "4:3" to 4f / 3)
89
+ ratios.forEach { (name, ratio) ->
90
+ val tile = LinearLayout(context).apply {
91
+ orientation = VERTICAL; gravity = Gravity.CENTER; isClickable = true; isFocusable = true; contentDescription = "$name crop ratio"
92
+ addView(object : android.view.View(context) {
93
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE; strokeWidth = dp(1).toFloat(); color = textColor }
94
+ override fun onDraw(canvas: Canvas) {
95
+ paint.color = if (this@apply.isSelected) accent else textColor
96
+ val aspect = if (name == "Original") originalRatio() else ratio ?: 1.25f
97
+ val w = minOf(dp(24).toFloat(), dp(24) * aspect); val h = w / aspect
98
+ canvas.drawRoundRect((width-w)/2, (height-h)/2, (width+w)/2, (height+h)/2, dp(2).toFloat(), dp(2).toFloat(), paint)
99
+ }
100
+ }, LayoutParams(dp(40), dp(38)))
101
+ addView(TextView(context).apply { text = name; textSize = 11f; gravity = Gravity.CENTER; setTextColor(textColor) })
102
+ setOnClickListener { onRatio(name, if (name == "Original") originalRatio() else ratio) }; applyPressScale()
103
+ }
104
+ presets[name] = tile
105
+ rail.addView(tile, LayoutParams(dp(64), dp(68)))
106
+ }
107
+ addView(HorizontalScrollView(context).apply { isHorizontalScrollBarEnabled = false; addView(rail) }, LayoutParams(LayoutParams.MATCH_PARENT, dp(72)))
108
+ }
109
+
110
+ fun sync(state: PhotoTransformState) {
111
+ syncing = true
112
+ slider.progress = (state.straightenDegrees * 10 + 450).roundToInt()
113
+ valueLabel.text = String.format(java.util.Locale.US, "%.1f°", state.straightenDegrees)
114
+ presets.forEach { (name, tile) ->
115
+ tile.isSelected = name == state.aspectPreset
116
+ tile.background = roundedDrawable(if (tile.isSelected) (accent and 0x00FFFFFF) or 0x22000000 else Color.TRANSPARENT, 12, context)
117
+ (tile.getChildAt(1) as TextView).setTextColor(if (tile.isSelected) accent else DesignTokens.textSecondary)
118
+ tile.getChildAt(0).invalidate()
119
+ }
120
+ syncing = false
121
+ }
122
+ }