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
@@ -1,172 +0,0 @@
1
- package com.photovideoeditor.stickers
2
-
3
- import android.content.Context
4
- import android.os.Handler
5
- import android.os.Looper
6
- import org.json.JSONArray
7
- import org.json.JSONObject
8
- import java.io.File
9
- import java.net.HttpURLConnection
10
- import java.net.URL
11
- import java.util.concurrent.Executors
12
-
13
- /** One entry from OpenMoji's `data/openmoji.json` index. */
14
- data class OpenMojiEntry(
15
- val emoji: String,
16
- val hexcode: String,
17
- val group: String,
18
- val annotation: String,
19
- val tags: String
20
- )
21
-
22
- /**
23
- * Fetches and disk-caches the OpenMoji catalog index (CC BY-SA 4.0, https://openmoji.org),
24
- * exposing search/filter helpers plus the CDN URL builders for thumbnail/full-res assets.
25
- *
26
- * Networking uses plain [HttpURLConnection] on a background executor (this codebase has no
27
- * networking dependency and shouldn't gain one for a simple GET) and parses with
28
- * [org.json.JSONArray]/[org.json.JSONObject], matching the JSON style already used elsewhere
29
- * (e.g. `PhotoVideoEditorActivity`'s `stickerAssets` parsing).
30
- */
31
- object OpenMojiCatalog {
32
- private const val INDEX_URL =
33
- "https://cdn.jsdelivr.net/gh/hfg-gmuend/openmoji@master/data/openmoji.json"
34
- private const val CACHE_FILE_NAME = "openmoji_index.json"
35
- private const val CACHE_TIMESTAMP_PREFS = "openmoji_cache"
36
- private const val CACHE_TIMESTAMP_KEY = "fetched_at"
37
- private val CACHE_TTL_MS = 7L * 24L * 60L * 60L * 1000L
38
-
39
- /** Fixed, sensible display order for the "All" chip row; unknown groups are appended after. */
40
- private val GROUP_DISPLAY_ORDER = listOf(
41
- "smileys-emotion",
42
- "people-body",
43
- "animals-nature",
44
- "food-drink",
45
- "travel-places",
46
- "activities",
47
- "objects",
48
- "symbols",
49
- "flags"
50
- )
51
-
52
- private val executor = Executors.newSingleThreadExecutor { task -> Thread(task, "openmoji-catalog") }
53
- private val mainHandler = Handler(Looper.getMainLooper())
54
-
55
- @Volatile private var cachedEntries: List<OpenMojiEntry>? = null
56
-
57
- /**
58
- * Loads the catalog (memory -> disk cache -> network, falling back to any cached copy on
59
- * network failure), delivering the result on the main thread.
60
- */
61
- fun load(context: Context, onResult: (entries: List<OpenMojiEntry>?, error: Exception?) -> Unit) {
62
- val inMemory = cachedEntries
63
- if (inMemory != null) {
64
- mainHandler.post { onResult(inMemory, null) }
65
- return
66
- }
67
- val appContext = context.applicationContext
68
- executor.execute {
69
- try {
70
- val entries = loadBlocking(appContext)
71
- cachedEntries = entries
72
- mainHandler.post { onResult(entries, null) }
73
- } catch (e: Exception) {
74
- mainHandler.post { onResult(null, e) }
75
- }
76
- }
77
- }
78
-
79
- private fun loadBlocking(context: Context): List<OpenMojiEntry> {
80
- val cacheFile = File(context.cacheDir, CACHE_FILE_NAME)
81
- val cacheAgeMs = cacheAgeMs(context)
82
- val cacheFresh = cacheFile.exists() && cacheAgeMs != null && cacheAgeMs < CACHE_TTL_MS
83
-
84
- if (cacheFresh) {
85
- return parse(cacheFile.readText())
86
- }
87
-
88
- return try {
89
- val json = fetch(INDEX_URL)
90
- cacheFile.writeText(json)
91
- markCacheTimestamp(context)
92
- parse(json)
93
- } catch (e: Exception) {
94
- // Network failed: fall back to any cached copy, regardless of age, per spec.
95
- if (cacheFile.exists()) parse(cacheFile.readText()) else throw e
96
- }
97
- }
98
-
99
- private fun fetch(urlString: String): String {
100
- val connection = URL(urlString).openConnection() as HttpURLConnection
101
- connection.instanceFollowRedirects = true
102
- connection.connectTimeout = 15_000
103
- connection.readTimeout = 30_000
104
- connection.requestMethod = "GET"
105
- try {
106
- val code = connection.responseCode
107
- if (code !in 200..299) throw java.io.IOException("HTTP $code fetching $urlString")
108
- return connection.inputStream.bufferedReader().use { it.readText() }
109
- } finally {
110
- connection.disconnect()
111
- }
112
- }
113
-
114
- private fun parse(json: String): List<OpenMojiEntry> {
115
- val array = JSONArray(json)
116
- val result = ArrayList<OpenMojiEntry>(array.length())
117
- for (i in 0 until array.length()) {
118
- val obj: JSONObject = array.optJSONObject(i) ?: continue
119
- val hexcode = obj.optString("hexcode").takeIf { it.isNotBlank() } ?: continue
120
- result.add(
121
- OpenMojiEntry(
122
- emoji = obj.optString("emoji"),
123
- hexcode = hexcode,
124
- group = obj.optString("group"),
125
- annotation = obj.optString("annotation"),
126
- tags = obj.optString("tags")
127
- )
128
- )
129
- }
130
- return result
131
- }
132
-
133
- private fun cacheAgeMs(context: Context): Long? {
134
- val prefs = context.getSharedPreferences(CACHE_TIMESTAMP_PREFS, Context.MODE_PRIVATE)
135
- val fetchedAt = prefs.getLong(CACHE_TIMESTAMP_KEY, -1L)
136
- if (fetchedAt < 0) return null
137
- return System.currentTimeMillis() - fetchedAt
138
- }
139
-
140
- private fun markCacheTimestamp(context: Context) {
141
- context.getSharedPreferences(CACHE_TIMESTAMP_PREFS, Context.MODE_PRIVATE)
142
- .edit()
143
- .putLong(CACHE_TIMESTAMP_KEY, System.currentTimeMillis())
144
- .apply()
145
- }
146
-
147
- fun groups(): List<String> {
148
- val entries = cachedEntries ?: return emptyList()
149
- val present = entries.map { it.group }.distinct()
150
- val ordered = GROUP_DISPLAY_ORDER.filter { it in present }
151
- val rest = present.filter { it !in GROUP_DISPLAY_ORDER }.sorted()
152
- return ordered + rest
153
- }
154
-
155
- fun byGroup(group: String): List<OpenMojiEntry> =
156
- cachedEntries?.filter { it.group == group } ?: emptyList()
157
-
158
- fun search(query: String): List<OpenMojiEntry> {
159
- val entries = cachedEntries ?: return emptyList()
160
- val needle = query.trim().lowercase()
161
- if (needle.isEmpty()) return entries
162
- return entries.filter {
163
- it.annotation.lowercase().contains(needle) || it.tags.lowercase().contains(needle)
164
- }
165
- }
166
-
167
- fun thumbnailUrl(hexcode: String): String =
168
- "https://cdn.jsdelivr.net/gh/hfg-gmuend/openmoji@master/color/72x72/$hexcode.png"
169
-
170
- fun fullUrl(hexcode: String): String =
171
- "https://cdn.jsdelivr.net/gh/hfg-gmuend/openmoji@master/color/618x618/$hexcode.png"
172
- }
@@ -1,110 +0,0 @@
1
- package com.photovideoeditor.stickers
2
-
3
- import android.content.Context
4
- import android.graphics.Bitmap
5
- import android.graphics.BitmapFactory
6
- import android.os.Handler
7
- import android.os.Looper
8
- import android.util.LruCache
9
- import android.widget.ImageView
10
- import java.io.File
11
- import java.io.FileOutputStream
12
- import java.net.HttpURLConnection
13
- import java.net.URL
14
- import java.security.MessageDigest
15
- import java.util.concurrent.Executors
16
-
17
- /**
18
- * Small async image pipeline for the online-stickers sheet: thumbnails for the grid (memory LRU
19
- * + disk cache) and full-res download-on-tap for the sticker that gets inserted as a layer.
20
- *
21
- * No image library dependency (Glide/Coil/Picasso) is used, matching this codebase's
22
- * dependency-light approach elsewhere.
23
- */
24
- object RemoteImageLoader {
25
- private val executor = Executors.newFixedThreadPool(4) { task -> Thread(task, "openmoji-image") }
26
- private val mainHandler = Handler(Looper.getMainLooper())
27
-
28
- private val memoryCache = object : LruCache<String, Bitmap>(8 * 1024 * 1024) {
29
- override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
30
- }
31
-
32
- private fun diskCacheDir(context: Context): File =
33
- File(context.cacheDir, "openmoji").apply { mkdirs() }
34
-
35
- private fun cacheFileFor(context: Context, url: String): File {
36
- val name = MessageDigest.getInstance("MD5").digest(url.toByteArray())
37
- .joinToString("") { "%02x".format(it) }
38
- return File(diskCacheDir(context), "$name.png")
39
- }
40
-
41
- /**
42
- * Loads [url] into [imageView], tagging the view with the URL being loaded so a recycled
43
- * RecyclerView cell doesn't get a stale bitmap set on it after the fact.
44
- */
45
- fun loadThumbnail(context: Context, url: String, imageView: ImageView) {
46
- imageView.tag = url
47
- val memoryHit = memoryCache.get(url)
48
- if (memoryHit != null) {
49
- imageView.setImageBitmap(memoryHit)
50
- return
51
- }
52
- imageView.setImageDrawable(null)
53
- val appContext = context.applicationContext
54
- executor.execute {
55
- val bitmap = try {
56
- val cacheFile = cacheFileFor(appContext, url)
57
- if (cacheFile.exists()) {
58
- BitmapFactory.decodeFile(cacheFile.absolutePath)
59
- } else {
60
- val bytes = download(url)
61
- cacheFile.writeBytes(bytes)
62
- BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
63
- }
64
- } catch (_: Exception) {
65
- null
66
- }
67
- if (bitmap != null) {
68
- memoryCache.put(url, bitmap)
69
- mainHandler.post {
70
- if (imageView.tag == url) imageView.setImageBitmap(bitmap)
71
- }
72
- }
73
- }
74
- }
75
-
76
- /**
77
- * Downloads (or returns the cached copy of) the full-res sticker image, delivering the local
78
- * file path on the main thread.
79
- */
80
- fun downloadFull(context: Context, url: String, onResult: (localFilePath: String?, error: Exception?) -> Unit) {
81
- val appContext = context.applicationContext
82
- executor.execute {
83
- try {
84
- val cacheFile = cacheFileFor(appContext, url)
85
- if (!cacheFile.exists()) {
86
- val bytes = download(url)
87
- FileOutputStream(cacheFile).use { it.write(bytes) }
88
- }
89
- mainHandler.post { onResult(cacheFile.absolutePath, null) }
90
- } catch (e: Exception) {
91
- mainHandler.post { onResult(null, e) }
92
- }
93
- }
94
- }
95
-
96
- private fun download(urlString: String): ByteArray {
97
- val connection = URL(urlString).openConnection() as HttpURLConnection
98
- connection.instanceFollowRedirects = true
99
- connection.connectTimeout = 15_000
100
- connection.readTimeout = 30_000
101
- connection.requestMethod = "GET"
102
- try {
103
- val code = connection.responseCode
104
- if (code !in 200..299) throw java.io.IOException("HTTP $code fetching $urlString")
105
- return connection.inputStream.use { it.readBytes() }
106
- } finally {
107
- connection.disconnect()
108
- }
109
- }
110
- }
@@ -1,188 +0,0 @@
1
- import Foundation
2
-
3
- /// A single entry from OpenMoji's `openmoji.json` index that we care about.
4
- /// Mirrors Android's `OpenMojiCatalog.Entry` in spirit (same field set).
5
- struct OpenMojiEntry {
6
- let emoji: String
7
- let hexcode: String
8
- let group: String
9
- let annotation: String
10
- let tags: String
11
- }
12
-
13
- /// Fetches, disk-caches, and serves the OpenMoji sticker index
14
- /// (`https://cdn.jsdelivr.net/gh/hfg-gmuend/openmoji@master/data/openmoji.json`).
15
- ///
16
- /// No third-party networking/JSON dependency is used — this codebase is deliberately
17
- /// dependency-light (see `stickerAssets: [[String: Any]]` in `PhotoVideoEditorViewController`
18
- /// for the existing dictionary-based JSON precedent). Plain `URLSession` + `JSONSerialization`.
19
- final class OpenMojiCatalog {
20
- static let shared = OpenMojiCatalog()
21
-
22
- /// Fixed, sensible display order for OpenMoji's `group` values.
23
- static let orderedGroups: [String] = [
24
- "smileys-emotion",
25
- "people-body",
26
- "animals-nature",
27
- "food-drink",
28
- "travel-places",
29
- "activities",
30
- "objects",
31
- "symbols",
32
- "flags",
33
- ]
34
-
35
- private static let indexURL = URL(string: "https://cdn.jsdelivr.net/gh/hfg-gmuend/openmoji@master/data/openmoji.json")!
36
- private static let cacheFileName = "openmoji_index.json"
37
- private static let cacheTimestampKey = "OpenMojiCatalog.cacheTimestamp"
38
- private static let cacheMaxAgeSeconds: TimeInterval = 7 * 24 * 60 * 60 // 7 days
39
-
40
- private let queue = DispatchQueue(label: "com.photovideoeditor.openmojicatalog", qos: .userInitiated)
41
- private var entries: [OpenMojiEntry]?
42
- private var isLoading = false
43
- private var pendingCompletions: [([OpenMojiEntry]?, Error?) -> Void] = []
44
-
45
- private init() {}
46
-
47
- private var cacheFileURL: URL? {
48
- FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?
49
- .appendingPathComponent(Self.cacheFileName)
50
- }
51
-
52
- enum CatalogError: Error {
53
- case network(Error)
54
- case badResponse
55
- case emptyCache
56
- }
57
-
58
- /// Loads the catalog (cache-first, network fallback with cache-refresh) and calls `completion`
59
- /// on the main thread. Safe to call multiple times concurrently; duplicate in-flight calls are coalesced.
60
- func load(completion: @escaping ([OpenMojiEntry]?, Error?) -> Void) {
61
- if let entries {
62
- DispatchQueue.main.async { completion(entries, nil) }
63
- return
64
- }
65
- queue.async { [weak self] in
66
- guard let self else { return }
67
- self.pendingCompletions.append(completion)
68
- if self.isLoading { return }
69
- self.isLoading = true
70
- self.loadLocked()
71
- }
72
- }
73
-
74
- /// Must be called on `queue`. Decides whether to use the on-disk cache or fetch fresh, then
75
- /// fans results out to every completion queued while the load was in flight.
76
- private func loadLocked() {
77
- let cacheAge = self.cacheAgeSeconds()
78
- if let cacheAge, cacheAge < Self.cacheMaxAgeSeconds, let cached = self.readCache() {
79
- self.finishLoad(entries: cached, error: nil)
80
- return
81
- }
82
- self.fetchFromNetwork { [weak self] data, error in
83
- guard let self else { return }
84
- self.queue.async {
85
- if let data {
86
- self.writeCache(data)
87
- let parsed = self.parse(data)
88
- self.finishLoad(entries: parsed, error: parsed.isEmpty ? CatalogError.badResponse : nil)
89
- } else if let cached = self.readCache() {
90
- // Network failed but we have a cache of any age — fall back to it.
91
- self.finishLoad(entries: cached, error: nil)
92
- } else {
93
- self.finishLoad(entries: nil, error: error ?? CatalogError.emptyCache)
94
- }
95
- }
96
- }
97
- }
98
-
99
- /// Must be called on `queue`.
100
- private func finishLoad(entries: [OpenMojiEntry]?, error: Error?) {
101
- if let entries { self.entries = entries }
102
- let completions = self.pendingCompletions
103
- self.pendingCompletions = []
104
- self.isLoading = false
105
- DispatchQueue.main.async {
106
- completions.forEach { $0(entries, entries == nil ? error : nil) }
107
- }
108
- }
109
-
110
- private func fetchFromNetwork(completion: @escaping (Data?, Error?) -> Void) {
111
- let task = URLSession.shared.dataTask(with: Self.indexURL) { data, response, error in
112
- if let error {
113
- completion(nil, CatalogError.network(error))
114
- return
115
- }
116
- guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode), let data else {
117
- completion(nil, CatalogError.badResponse)
118
- return
119
- }
120
- completion(data, nil)
121
- }
122
- task.resume()
123
- }
124
-
125
- private func cacheAgeSeconds() -> TimeInterval? {
126
- let timestamp = UserDefaults.standard.double(forKey: Self.cacheTimestampKey)
127
- guard timestamp > 0 else { return nil }
128
- return Date().timeIntervalSince1970 - timestamp
129
- }
130
-
131
- private func readCache() -> [OpenMojiEntry]? {
132
- guard let url = cacheFileURL, let data = try? Data(contentsOf: url) else { return nil }
133
- let parsed = parse(data)
134
- return parsed.isEmpty ? nil : parsed
135
- }
136
-
137
- private func writeCache(_ data: Data) {
138
- guard let url = cacheFileURL else { return }
139
- do {
140
- try data.write(to: url, options: .atomic)
141
- UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: Self.cacheTimestampKey)
142
- } catch {
143
- // Best-effort cache; ignore write failures.
144
- }
145
- }
146
-
147
- private func parse(_ data: Data) -> [OpenMojiEntry] {
148
- guard let raw = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return [] }
149
- return raw.compactMap { dict in
150
- guard
151
- let hexcode = dict["hexcode"] as? String, !hexcode.isEmpty,
152
- let group = dict["group"] as? String
153
- else { return nil }
154
- let emoji = dict["emoji"] as? String ?? ""
155
- let annotation = dict["annotation"] as? String ?? ""
156
- let tags = dict["tags"] as? String ?? ""
157
- return OpenMojiEntry(emoji: emoji, hexcode: hexcode, group: group, annotation: annotation, tags: tags)
158
- }
159
- }
160
-
161
- // MARK: - Query surface
162
-
163
- func groups() -> [String] {
164
- guard let entries else { return [] }
165
- let present = Set(entries.map { $0.group })
166
- return Self.orderedGroups.filter { present.contains($0) }
167
- }
168
-
169
- func byGroup(_ group: String) -> [OpenMojiEntry] {
170
- (entries ?? []).filter { $0.group == group }
171
- }
172
-
173
- func search(_ query: String) -> [OpenMojiEntry] {
174
- let needle = query.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
175
- guard !needle.isEmpty else { return entries ?? [] }
176
- return (entries ?? []).filter {
177
- $0.annotation.lowercased().contains(needle) || $0.tags.lowercased().contains(needle)
178
- }
179
- }
180
-
181
- func thumbnailURL(_ hexcode: String) -> URL {
182
- URL(string: "https://cdn.jsdelivr.net/gh/hfg-gmuend/openmoji@master/color/72x72/\(hexcode).png")!
183
- }
184
-
185
- func fullURL(_ hexcode: String) -> URL {
186
- URL(string: "https://cdn.jsdelivr.net/gh/hfg-gmuend/openmoji@master/color/618x618/\(hexcode).png")!
187
- }
188
- }
@@ -1,127 +0,0 @@
1
- import UIKit
2
-
3
- /// Small async image loader for the online-sticker sheet: in-memory + on-disk thumbnail cache,
4
- /// plus a full-res "download and hand me a local file" path for the sticker that actually gets
5
- /// inserted as a layer. Plain `URLSession`, no third-party dependency (matches the rest of the
6
- /// codebase's zero-networking-dependency policy).
7
- final class RemoteImageLoader {
8
- static let shared = RemoteImageLoader()
9
-
10
- private let memoryCache = NSCache<NSString, UIImage>()
11
- private let ioQueue = DispatchQueue(label: "com.photovideoeditor.remoteimageloader.io", qos: .userInitiated)
12
- private let session = URLSession.shared
13
-
14
- private init() {
15
- memoryCache.countLimit = 500
16
- }
17
-
18
- private var diskCacheDirectory: URL? {
19
- guard let base = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { return nil }
20
- let dir = base.appendingPathComponent("openmoji", isDirectory: true)
21
- if !FileManager.default.fileExists(atPath: dir.path) {
22
- try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
23
- }
24
- return dir
25
- }
26
-
27
- private func diskCacheFile(forKey key: String) -> URL? {
28
- diskCacheDirectory?.appendingPathComponent(key)
29
- }
30
-
31
- // MARK: - Thumbnails (UICollectionViewCell-safe)
32
-
33
- /// Loads a thumbnail into `imageView`, guarding against cell reuse: the URL being loaded is
34
- /// stamped onto the image view via associated object, and the result is only applied if that
35
- /// stamp still matches once the load completes (standard fix for the "wrong image after scroll"
36
- /// bug with recycled `UICollectionViewCell`/`UITableViewCell` image views).
37
- func loadThumbnail(url: URL, into imageView: UIImageView) {
38
- Self.setLoadTag(url.absoluteString, on: imageView)
39
- imageView.image = nil
40
-
41
- let cacheKey = url.absoluteString as NSString
42
- if let cached = memoryCache.object(forKey: cacheKey) {
43
- imageView.image = cached
44
- return
45
- }
46
-
47
- let diskKey = "\(url.lastPathComponent)_thumb.png"
48
- ioQueue.async { [weak self] in
49
- guard let self else { return }
50
- if let diskURL = self.diskCacheFile(forKey: diskKey),
51
- let data = try? Data(contentsOf: diskURL),
52
- let image = UIImage(data: data) {
53
- self.memoryCache.setObject(image, forKey: cacheKey)
54
- DispatchQueue.main.async {
55
- guard Self.loadTag(on: imageView) == url.absoluteString else { return }
56
- imageView.image = image
57
- }
58
- return
59
- }
60
-
61
- self.session.dataTask(with: url) { data, response, _ in
62
- guard
63
- let data, let image = UIImage(data: data),
64
- let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode)
65
- else { return }
66
- self.memoryCache.setObject(image, forKey: cacheKey)
67
- if let diskURL = self.diskCacheFile(forKey: diskKey) {
68
- try? data.write(to: diskURL, options: .atomic)
69
- }
70
- DispatchQueue.main.async {
71
- guard Self.loadTag(on: imageView) == url.absoluteString else { return }
72
- imageView.image = image
73
- }
74
- }.resume()
75
- }
76
- }
77
-
78
- // MARK: - Full-res download
79
-
80
- /// Downloads (or returns the already-cached file for) the full-res sticker image. Completion
81
- /// always fires on the main thread.
82
- func downloadFull(url: URL, completion: @escaping (URL?, Error?) -> Void) {
83
- let diskKey = "\(url.lastPathComponent)_full.png"
84
- ioQueue.async { [weak self] in
85
- guard let self else { return }
86
- if let diskURL = self.diskCacheFile(forKey: diskKey), FileManager.default.fileExists(atPath: diskURL.path) {
87
- DispatchQueue.main.async { completion(diskURL, nil) }
88
- return
89
- }
90
- self.session.dataTask(with: url) { data, response, error in
91
- if let error {
92
- DispatchQueue.main.async { completion(nil, error) }
93
- return
94
- }
95
- guard
96
- let data,
97
- let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode)
98
- else {
99
- DispatchQueue.main.async { completion(nil, URLError(.badServerResponse)) }
100
- return
101
- }
102
- guard let diskURL = self.diskCacheFile(forKey: diskKey) else {
103
- DispatchQueue.main.async { completion(nil, URLError(.cannotCreateFile)) }
104
- return
105
- }
106
- do {
107
- try data.write(to: diskURL, options: .atomic)
108
- DispatchQueue.main.async { completion(diskURL, nil) }
109
- } catch {
110
- DispatchQueue.main.async { completion(nil, error) }
111
- }
112
- }.resume()
113
- }
114
- }
115
-
116
- // MARK: - Cell-reuse tagging
117
-
118
- private static var loadTagKey: UInt8 = 0
119
-
120
- private static func setLoadTag(_ tag: String, on imageView: UIImageView) {
121
- objc_setAssociatedObject(imageView, &loadTagKey, tag, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
122
- }
123
-
124
- private static func loadTag(on imageView: UIImageView) -> String? {
125
- objc_getAssociatedObject(imageView, &loadTagKey) as? String
126
- }
127
- }