react-native-photo-video-editor 0.1.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 (162) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/CONTRIBUTING.md +101 -0
  3. package/LICENSE +20 -0
  4. package/PhotoVideoEditor.podspec +23 -0
  5. package/README.md +53 -0
  6. package/android/build.gradle +61 -0
  7. package/android/src/main/AndroidManifest.xml +12 -0
  8. package/android/src/main/java/com/photovideoeditor/PhotoVideoEditorModule.kt +126 -0
  9. package/android/src/main/java/com/photovideoeditor/PhotoVideoEditorPackage.kt +31 -0
  10. package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +2521 -0
  11. package/android/src/main/java/com/photovideoeditor/editor/README.md +5 -0
  12. package/android/src/main/java/com/photovideoeditor/files/SourceResolver.kt +29 -0
  13. package/android/src/main/java/com/photovideoeditor/media/BitmapSampling.kt +11 -0
  14. package/android/src/main/java/com/photovideoeditor/model/EditorRequest.kt +3 -0
  15. package/android/src/main/java/com/photovideoeditor/photo/export/PhotoExporter.kt +201 -0
  16. package/android/src/main/java/com/photovideoeditor/photo/render/OverlayGeometry.kt +22 -0
  17. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoAdjustmentRenderer.kt +160 -0
  18. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoAdjustments.kt +55 -0
  19. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoEditSession.kt +121 -0
  20. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoFilterPresets.kt +64 -0
  21. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayer.kt +54 -0
  22. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +108 -0
  23. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerStack.kt +60 -0
  24. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformState.kt +52 -0
  25. package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +168 -0
  26. package/android/src/main/java/com/photovideoeditor/photo/ui/FilterThumbnailLoader.kt +57 -0
  27. package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +202 -0
  28. package/android/src/main/java/com/photovideoeditor/photo/ui/PhotoPreviewRenderCoordinator.kt +57 -0
  29. package/android/src/main/java/com/photovideoeditor/photo/ui/ZoomableImageView.kt +96 -0
  30. package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +310 -0
  31. package/android/src/main/java/com/photovideoeditor/stickers/OpenMojiCatalog.kt +172 -0
  32. package/android/src/main/java/com/photovideoeditor/stickers/RemoteImageLoader.kt +110 -0
  33. package/android/src/main/java/com/photovideoeditor/ui/DesignTokens.kt +93 -0
  34. package/android/src/main/java/com/photovideoeditor/ui/components/EditorComponents.kt +119 -0
  35. package/android/src/main/java/com/photovideoeditor/validation/EditorRequestValidator.kt +11 -0
  36. package/android/src/main/java/com/photovideoeditor/video/cache/TimelineThumbnailRepository.kt +130 -0
  37. package/android/src/main/java/com/photovideoeditor/video/export/VideoExporter.kt +319 -0
  38. package/android/src/main/java/com/photovideoeditor/video/render/VideoClip.kt +20 -0
  39. package/android/src/main/java/com/photovideoeditor/video/render/VideoEditSession.kt +136 -0
  40. package/android/src/main/java/com/photovideoeditor/video/render/VideoThumbnailGenerator.kt +31 -0
  41. package/android/src/main/java/com/photovideoeditor/video/render/VideoTransformState.kt +48 -0
  42. package/android/src/main/java/com/photovideoeditor/video/ui/TrimRangeView.kt +104 -0
  43. package/android/src/main/res/drawable/ic_add.xml +9 -0
  44. package/android/src/main/res/drawable/ic_arrow_back.xml +9 -0
  45. package/android/src/main/res/drawable/ic_aspect_ratio.xml +9 -0
  46. package/android/src/main/res/drawable/ic_auto_fix_high.xml +9 -0
  47. package/android/src/main/res/drawable/ic_check.xml +9 -0
  48. package/android/src/main/res/drawable/ic_chevron_left.xml +9 -0
  49. package/android/src/main/res/drawable/ic_chevron_right.xml +9 -0
  50. package/android/src/main/res/drawable/ic_close.xml +9 -0
  51. package/android/src/main/res/drawable/ic_compare.xml +9 -0
  52. package/android/src/main/res/drawable/ic_content_copy.xml +9 -0
  53. package/android/src/main/res/drawable/ic_content_cut.xml +9 -0
  54. package/android/src/main/res/drawable/ic_crop.xml +9 -0
  55. package/android/src/main/res/drawable/ic_crop_square.xml +9 -0
  56. package/android/src/main/res/drawable/ic_delete.xml +9 -0
  57. package/android/src/main/res/drawable/ic_drag_indicator.xml +9 -0
  58. package/android/src/main/res/drawable/ic_face.xml +9 -0
  59. package/android/src/main/res/drawable/ic_filter_center_focus.xml +9 -0
  60. package/android/src/main/res/drawable/ic_flare.xml +9 -0
  61. package/android/src/main/res/drawable/ic_format_size.xml +9 -0
  62. package/android/src/main/res/drawable/ic_fullscreen.xml +9 -0
  63. package/android/src/main/res/drawable/ic_layers.xml +9 -0
  64. package/android/src/main/res/drawable/ic_light_mode.xml +9 -0
  65. package/android/src/main/res/drawable/ic_lock.xml +9 -0
  66. package/android/src/main/res/drawable/ic_magic_button.xml +9 -0
  67. package/android/src/main/res/drawable/ic_movie.xml +9 -0
  68. package/android/src/main/res/drawable/ic_opacity.xml +9 -0
  69. package/android/src/main/res/drawable/ic_palette.xml +9 -0
  70. package/android/src/main/res/drawable/ic_photo_filter.xml +9 -0
  71. package/android/src/main/res/drawable/ic_redo.xml +9 -0
  72. package/android/src/main/res/drawable/ic_rotate_90_degrees_ccw.xml +9 -0
  73. package/android/src/main/res/drawable/ic_rotate_left.xml +9 -0
  74. package/android/src/main/res/drawable/ic_rotate_right.xml +9 -0
  75. package/android/src/main/res/drawable/ic_sentiment_satisfied.xml +9 -0
  76. package/android/src/main/res/drawable/ic_share.xml +9 -0
  77. package/android/src/main/res/drawable/ic_speed.xml +9 -0
  78. package/android/src/main/res/drawable/ic_straighten.xml +9 -0
  79. package/android/src/main/res/drawable/ic_title.xml +9 -0
  80. package/android/src/main/res/drawable/ic_tune.xml +9 -0
  81. package/android/src/main/res/drawable/ic_undo.xml +9 -0
  82. package/android/src/main/res/drawable/ic_visibility_off.xml +9 -0
  83. package/android/src/main/res/drawable/ic_wallpaper.xml +9 -0
  84. package/android/src/main/res/values/styles.xml +10 -0
  85. package/docs/api.md +9 -0
  86. package/docs/architecture.md +9 -0
  87. package/docs/customization.md +18 -0
  88. package/docs/development.md +21 -0
  89. package/docs/formats.md +27 -0
  90. package/docs/performance.md +3 -0
  91. package/docs/permissions.md +3 -0
  92. package/docs/photo-editor.md +41 -0
  93. package/docs/roadmap.md +12 -0
  94. package/docs/technology-decisions.md +17 -0
  95. package/docs/video-editor.md +76 -0
  96. package/ios/Editor/DesignTokens.swift +75 -0
  97. package/ios/Editor/PhotoVideoEditorOutcome.swift +9 -0
  98. package/ios/Editor/PhotoVideoEditorViewController.swift +2064 -0
  99. package/ios/Editor/README.md +5 -0
  100. package/ios/Editor/UI/EditorComponents.swift +138 -0
  101. package/ios/Files/SourceResolver.swift +13 -0
  102. package/ios/Models/EditorRequest.swift +3 -0
  103. package/ios/Photo/Export/PhotoExporter.swift +144 -0
  104. package/ios/Photo/Rendering/OverlayGeometry.swift +22 -0
  105. package/ios/Photo/Rendering/PhotoAdjustmentRenderer.swift +141 -0
  106. package/ios/Photo/Rendering/PhotoAdjustments.swift +57 -0
  107. package/ios/Photo/Rendering/PhotoEditSession.swift +121 -0
  108. package/ios/Photo/Rendering/PhotoFilterPresets.swift +45 -0
  109. package/ios/Photo/Rendering/PhotoLayer.swift +59 -0
  110. package/ios/Photo/Rendering/PhotoLayerRenderer.swift +94 -0
  111. package/ios/Photo/Rendering/PhotoLayerStack.swift +60 -0
  112. package/ios/Photo/Rendering/PhotoTransformState.swift +49 -0
  113. package/ios/Photo/Rendering/PixelBuffer.swift +68 -0
  114. package/ios/Photo/UI/CropOverlayView.swift +161 -0
  115. package/ios/Photo/UI/LayerOverlayView.swift +176 -0
  116. package/ios/Photo/UI/ZoomableImageView.swift +79 -0
  117. package/ios/PhotoVideoEditor.mm +20 -0
  118. package/ios/PhotoVideoEditor.swift +70 -0
  119. package/ios/Stickers/OnlineStickerSheetViewController.swift +418 -0
  120. package/ios/Stickers/OpenMojiCatalog.swift +188 -0
  121. package/ios/Stickers/RemoteImageLoader.swift +127 -0
  122. package/ios/Validation/EditorRequestValidator.swift +9 -0
  123. package/ios/Video/Cache/TimelineThumbnailRepository.swift +148 -0
  124. package/ios/Video/Export/VideoExporter.swift +275 -0
  125. package/ios/Video/Render/VideoClip.swift +21 -0
  126. package/ios/Video/Render/VideoEditSession.swift +99 -0
  127. package/ios/Video/Render/VideoThumbnailGenerator.swift +25 -0
  128. package/ios/Video/Render/VideoTransformState.swift +23 -0
  129. package/ios/Video/UI/TrimRangeView.swift +91 -0
  130. package/lib/module/NativePhotoVideoEditor.js +5 -0
  131. package/lib/module/NativePhotoVideoEditor.js.map +1 -0
  132. package/lib/module/errors.js +29 -0
  133. package/lib/module/errors.js.map +1 -0
  134. package/lib/module/events.js +27 -0
  135. package/lib/module/events.js.map +1 -0
  136. package/lib/module/index.js +91 -0
  137. package/lib/module/index.js.map +1 -0
  138. package/lib/module/package.json +1 -0
  139. package/lib/module/project.js +4 -0
  140. package/lib/module/project.js.map +1 -0
  141. package/lib/module/types.js +2 -0
  142. package/lib/module/types.js.map +1 -0
  143. package/lib/typescript/package.json +1 -0
  144. package/lib/typescript/src/NativePhotoVideoEditor.d.ts +11 -0
  145. package/lib/typescript/src/NativePhotoVideoEditor.d.ts.map +1 -0
  146. package/lib/typescript/src/errors.d.ts +11 -0
  147. package/lib/typescript/src/errors.d.ts.map +1 -0
  148. package/lib/typescript/src/events.d.ts +5 -0
  149. package/lib/typescript/src/events.d.ts.map +1 -0
  150. package/lib/typescript/src/index.d.ts +15 -0
  151. package/lib/typescript/src/index.d.ts.map +1 -0
  152. package/lib/typescript/src/project.d.ts +24 -0
  153. package/lib/typescript/src/project.d.ts.map +1 -0
  154. package/lib/typescript/src/types.d.ts +108 -0
  155. package/lib/typescript/src/types.d.ts.map +1 -0
  156. package/package.json +154 -0
  157. package/src/NativePhotoVideoEditor.ts +12 -0
  158. package/src/errors.ts +78 -0
  159. package/src/events.ts +43 -0
  160. package/src/index.ts +123 -0
  161. package/src/project.ts +17 -0
  162. package/src/types.ts +121 -0
@@ -0,0 +1,172 @@
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
+ }
@@ -0,0 +1,110 @@
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
+ }
@@ -0,0 +1,93 @@
1
+ package com.photovideoeditor.ui
2
+
3
+ import android.graphics.Color
4
+
5
+ /**
6
+ * Default values for the "Studio Violet" design system used by the native editor
7
+ * shell, matching the Stitch-exported mockups under
8
+ * stitch_react_native_photo_editor_sdk/studio_violet_creative_sdk/DESIGN.md.
9
+ * These are FALLBACKS only — every color a consumer can already override via
10
+ * `EditorOptions.theme` (see `PhotoVideoEditorActivity.themeColor`) still wins;
11
+ * this object just changes what the editor looks like when no override is given.
12
+ */
13
+ object DesignTokens {
14
+ // Surface hierarchy
15
+ const val surfaceDim = 0xFF131316.toInt()
16
+ const val surface = 0xFF131316.toInt()
17
+ const val surfaceContainerLowest = 0xFF0E0E11.toInt()
18
+ const val surfaceContainerLow = 0xFF1B1B1E.toInt()
19
+ const val surfaceContainer = 0xFF1F1F22.toInt()
20
+ const val surfaceContainerHigh = 0xFF2A2A2D.toInt()
21
+ const val surfaceContainerHighest = 0xFF353438.toInt()
22
+ const val surfaceVariant = 0xFF353438.toInt()
23
+ const val surfaceBright = 0xFF39393C.toInt()
24
+
25
+ // Content
26
+ const val onSurface = 0xFFE4E1E6.toInt()
27
+ const val onSurfaceVariant = 0xFFCCC3D8.toInt()
28
+ const val outline = 0xFF958DA1.toInt()
29
+ const val outlineVariant = 0xFF4A4455.toInt()
30
+
31
+ // Accents
32
+ const val primary = 0xFFD2BBFF.toInt()
33
+ const val onPrimary = 0xFF3F008E.toInt()
34
+ const val primaryContainer = 0xFF7C3AED.toInt()
35
+ const val onPrimaryContainer = 0xFFEDE0FF.toInt()
36
+ const val secondary = 0xFFC0C1FF.toInt()
37
+ const val tertiary = 0xFFD0BCFF.toInt()
38
+ const val error = 0xFFFFB4AB.toInt()
39
+ const val errorContainer = 0xFF93000A.toInt()
40
+
41
+ // Literal Stitch accent hexes (DESIGN.md "Primary Accents & Active States"), used by the
42
+ // new EditorUI components below where the exact mockup hue matters more than the M3 mapping.
43
+ const val accentViolet = 0xFF7C3AED.toInt()
44
+ const val accentIndigo = 0xFF6366F1.toInt()
45
+ const val accentHighlightViolet = 0xFF8B5CF6.toInt()
46
+
47
+ // Literal Stitch surface hexes (DESIGN.md "Canvas & Surface Hierarchy").
48
+ const val canvasViewport = 0xFF09090B.toInt()
49
+ const val editorSurface = 0xFF121214.toInt()
50
+ const val elevatedPanel = 0xFF18181B.toInt()
51
+ const val interactiveNeutral = 0xFF27272A.toInt()
52
+ const val mutedBorderColor = 0xFF3F3F46.toInt()
53
+
54
+ // Literal Stitch text hexes (DESIGN.md "Typography & Content Grayscale").
55
+ const val textPrimary = 0xFFFFFFFF.toInt()
56
+ const val textSecondary = 0xFFA1A1AA.toInt()
57
+ const val textSubtle = 0xFF71717A.toInt()
58
+
59
+ // Backwards-compatible aliases matching the flatter names `themeColor()` fallbacks used before this pass.
60
+ const val toolbarColor = surfaceContainerLow
61
+ const val backgroundColor = surfaceContainerLowest
62
+ const val textColor = onSurface
63
+ const val primaryColor = primaryContainer
64
+
65
+ /** [color] at [alphaPercent]% opacity (0..100), matching the mockups' translucent surface tiers. */
66
+ fun withAlphaPercent(color: Int, alphaPercent: Int): Int {
67
+ val alpha = ((alphaPercent.coerceIn(0, 100)) * 255 / 100)
68
+ return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
69
+ }
70
+
71
+ // Spacing (dp), matching DESIGN.md's `space-*` scale.
72
+ const val spaceXs = 4
73
+ const val spaceSm = 8
74
+ const val spaceMd = 12
75
+ const val spaceLg = 16
76
+ const val spaceXl = 20
77
+ const val space2xl = 24
78
+ const val space3xl = 32
79
+ const val touchTargetMin = 44
80
+
81
+ // Corner radii (dp)
82
+ const val radiusSm = 4
83
+ const val radiusMd = 8
84
+ const val radiusLg = 12
85
+ const val radiusXl = 16
86
+ const val radiusSheet = 24
87
+
88
+ // Elevation tiers (DESIGN.md "Elevation & Depth"): background color + opacity percent
89
+ // to composite persistent chrome / floating trays / popovers over the canvas.
90
+ object ElevationTier1 { const val color = editorSurface; const val opacityPercent = 92 }
91
+ object ElevationTier2 { const val color = elevatedPanel; const val opacityPercent = 88 }
92
+ object ElevationTier3 { const val color = interactiveNeutral; const val opacityPercent = 100 }
93
+ }
@@ -0,0 +1,119 @@
1
+ package com.photovideoeditor.ui.components
2
+
3
+ import android.content.Context
4
+ import android.graphics.Typeface
5
+ import android.graphics.drawable.GradientDrawable
6
+ import android.view.Gravity
7
+ import android.view.MotionEvent
8
+ import android.view.View
9
+ import android.widget.FrameLayout
10
+ import android.widget.ImageView
11
+ import android.widget.LinearLayout
12
+ import android.widget.TextView
13
+ import com.photovideoeditor.ui.DesignTokens
14
+
15
+ /**
16
+ * Shared "Studio Violet" UI primitives (DESIGN.md), reused across the photo/video editor shell
17
+ * instead of hand-rolling styling per screen. Kept as small factory functions rather than View
18
+ * subclasses to match this codebase's existing `create*`/`actionButton` builder style.
19
+ */
20
+
21
+ fun dpToPx(dp: Int, context: Context): Int = (dp * context.resources.displayMetrics.density).toInt()
22
+
23
+ fun roundedDrawable(
24
+ color: Int,
25
+ radiusDp: Int,
26
+ context: Context,
27
+ strokeColor: Int? = null,
28
+ strokeWidthDp: Int = 1
29
+ ): GradientDrawable = GradientDrawable().apply {
30
+ cornerRadius = dpToPx(radiusDp, context).toFloat()
31
+ setColor(color)
32
+ if (strokeColor != null) setStroke(dpToPx(strokeWidthDp, context).coerceAtLeast(1), strokeColor)
33
+ }
34
+
35
+ /**
36
+ * Tactile press feedback (scale 1.0 -> 0.97) matching DESIGN.md's "Tactile Instrument Feel".
37
+ * Returns false so the view's own click handling (existing `setOnClickListener`) still fires.
38
+ */
39
+ fun View.applyPressScale(pressedScale: Float = 0.97f) {
40
+ setOnTouchListener { v, event ->
41
+ when (event.actionMasked) {
42
+ MotionEvent.ACTION_DOWN -> v.animate().scaleX(pressedScale).scaleY(pressedScale).setDuration(80).start()
43
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL ->
44
+ v.animate().scaleX(1f).scaleY(1f).setDuration(120).start()
45
+ }
46
+ false
47
+ }
48
+ }
49
+
50
+ /** Elevated rounded container (DESIGN.md "Elevated Panels & Tiles"), used for cards/tiles/sheets. */
51
+ fun editorCard(
52
+ context: Context,
53
+ radiusDp: Int = DesignTokens.radiusLg,
54
+ backgroundColor: Int = DesignTokens.elevatedPanel,
55
+ borderColor: Int? = DesignTokens.interactiveNeutral
56
+ ): FrameLayout = FrameLayout(context).apply {
57
+ background = roundedDrawable(backgroundColor, radiusDp, context, strokeColor = borderColor)
58
+ }
59
+
60
+ /** Pill chip for quick actions / filter tags (DESIGN.md "Chips & Filter Presets"). */
61
+ fun editorChip(context: Context, text: String, selected: Boolean, onClick: () -> Unit): TextView =
62
+ TextView(context).apply {
63
+ this.text = text
64
+ textSize = 12f
65
+ gravity = Gravity.CENTER
66
+ minHeight = dpToPx(32, context)
67
+ setTextColor(if (selected) DesignTokens.textPrimary else DesignTokens.textSecondary)
68
+ background = roundedDrawable(
69
+ color = if (selected) DesignTokens.accentViolet else DesignTokens.elevatedPanel,
70
+ radiusDp = 16,
71
+ context = context,
72
+ strokeColor = if (selected) null else DesignTokens.interactiveNeutral
73
+ )
74
+ val padH = dpToPx(DesignTokens.spaceLg, context)
75
+ setPadding(padH, dpToPx(DesignTokens.spaceXs, context), padH, dpToPx(DesignTokens.spaceXs, context))
76
+ isClickable = true
77
+ isFocusable = true
78
+ setOnClickListener { onClick() }
79
+ applyPressScale()
80
+ }
81
+
82
+ /**
83
+ * Primary Export CTA (DESIGN.md "Primary Export Button"): solid violet fill, 44dp min height,
84
+ * 12px radius, subtle elevation glow, 0.97 press scale.
85
+ */
86
+ fun editorPrimaryButton(
87
+ context: Context,
88
+ text: String,
89
+ iconRes: Int? = null,
90
+ fillColor: Int = DesignTokens.accentViolet,
91
+ textColor: Int = DesignTokens.textPrimary,
92
+ onClick: () -> Unit
93
+ ): LinearLayout = LinearLayout(context).apply {
94
+ orientation = LinearLayout.HORIZONTAL
95
+ gravity = Gravity.CENTER
96
+ background = roundedDrawable(fillColor, DesignTokens.radiusLg, context)
97
+ elevation = dpToPx(4, context).toFloat()
98
+ minimumHeight = dpToPx(DesignTokens.touchTargetMin, context)
99
+ setPadding(dpToPx(DesignTokens.spaceLg, context), 0, dpToPx(DesignTokens.spaceLg, context), 0)
100
+ isClickable = true
101
+ isFocusable = true
102
+ if (iconRes != null) {
103
+ addView(
104
+ ImageView(context).apply {
105
+ setImageResource(iconRes)
106
+ setColorFilter(textColor)
107
+ },
108
+ LinearLayout.LayoutParams(dpToPx(18, context), dpToPx(18, context)).apply { marginEnd = dpToPx(DesignTokens.spaceXs, context) }
109
+ )
110
+ }
111
+ addView(TextView(context).apply {
112
+ this.text = text
113
+ setTextColor(textColor)
114
+ textSize = 13f
115
+ setTypeface(typeface, Typeface.BOLD)
116
+ })
117
+ setOnClickListener { onClick() }
118
+ applyPressScale()
119
+ }
@@ -0,0 +1,11 @@
1
+ package com.photovideoeditor.validation
2
+
3
+ import com.photovideoeditor.model.EditorRequest
4
+
5
+ internal object EditorRequestValidator {
6
+ fun validate(request: EditorRequest): String? = when {
7
+ request.uri.isBlank() -> "E_INVALID_URI"
8
+ request.mediaType != "photo" && request.mediaType != "video" -> "E_UNSUPPORTED_MEDIA_TYPE"
9
+ else -> null
10
+ }
11
+ }
@@ -0,0 +1,130 @@
1
+ package com.photovideoeditor.video.cache
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.BitmapFactory
6
+ import android.media.MediaMetadataRetriever
7
+ import android.net.Uri
8
+ import android.os.Build
9
+ import android.util.LruCache
10
+ import java.io.File
11
+ import java.security.MessageDigest
12
+ import java.util.concurrent.ConcurrentHashMap
13
+ import java.util.concurrent.Executors
14
+ import java.util.concurrent.atomic.AtomicBoolean
15
+
16
+ object TimelineThumbnailKey {
17
+ fun create(mediaId: String, timestampMs: Long, width: Int, height: Int): String =
18
+ "timeline_${mediaId.sha256()}_${timestampMs}_${width}x$height"
19
+
20
+ private fun String.sha256(): String = MessageDigest.getInstance("SHA-256")
21
+ .digest(toByteArray()).take(12).joinToString("") { "%02x".format(it) }
22
+ }
23
+
24
+ /** Bounded memory -> disk -> decoder pipeline for timeline thumbnails. */
25
+ class TimelineThumbnailRepository(
26
+ context: Context,
27
+ private val maxDiskBytes: Long = 96L * 1024L * 1024L,
28
+ ) {
29
+ class Request internal constructor() {
30
+ private val cancelled = AtomicBoolean(false)
31
+ fun cancel() { cancelled.set(true) }
32
+ internal fun isCancelled() = cancelled.get()
33
+ }
34
+
35
+ private val appContext = context.applicationContext
36
+ private val cacheDir = File(appContext.cacheDir, "pve_timeline_thumbnails").apply { mkdirs() }
37
+ private val executor = Executors.newFixedThreadPool(2) { task ->
38
+ Thread(task, "timeline-thumbnail").apply { priority = Thread.NORM_PRIORITY - 1 }
39
+ }
40
+ private val requests = ConcurrentHashMap.newKeySet<Request>()
41
+ private val memoryLimitKb = (Runtime.getRuntime().maxMemory() / 16 / 1024)
42
+ .coerceIn(4L * 1024L, 64L * 1024L).toInt()
43
+ private val memory = object : LruCache<String, Bitmap>(memoryLimitKb) {
44
+ override fun sizeOf(key: String, value: Bitmap): Int = value.allocationByteCount / 1024
45
+ }
46
+
47
+ fun load(
48
+ mediaId: String,
49
+ uri: Uri,
50
+ durationMs: Long,
51
+ count: Int,
52
+ width: Int,
53
+ height: Int,
54
+ onThumbnail: (index: Int, bitmap: Bitmap) -> Unit,
55
+ onComplete: () -> Unit,
56
+ ): Request {
57
+ val request = Request()
58
+ requests.add(request)
59
+ executor.execute {
60
+ val retriever = MediaMetadataRetriever()
61
+ try {
62
+ retriever.setDataSource(appContext, uri)
63
+ val safeCount = count.coerceIn(1, 24)
64
+ val step = durationMs.coerceAtLeast(1) / safeCount
65
+ for (index in 0 until safeCount) {
66
+ if (request.isCancelled()) break
67
+ val timestampMs = index * step + step / 2
68
+ val key = TimelineThumbnailKey.create(mediaId, timestampMs, width, height)
69
+ val bitmap = memory.get(key) ?: readDisk(key) ?: decode(retriever, timestampMs, width, height)?.also {
70
+ memory.put(key, it)
71
+ writeDisk(key, it)
72
+ }
73
+ if (bitmap != null && !request.isCancelled()) onThumbnail(index, bitmap)
74
+ }
75
+ if (!request.isCancelled()) onComplete()
76
+ } catch (_: Exception) {
77
+ if (!request.isCancelled()) onComplete()
78
+ } finally {
79
+ retriever.release()
80
+ requests.remove(request)
81
+ trimDisk()
82
+ }
83
+ }
84
+ return request
85
+ }
86
+
87
+ private fun decode(retriever: MediaMetadataRetriever, timestampMs: Long, width: Int, height: Int): Bitmap? {
88
+ val frame = if (Build.VERSION.SDK_INT >= 27) {
89
+ retriever.getScaledFrameAtTime(timestampMs * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, width, height)
90
+ } else {
91
+ retriever.getFrameAtTime(timestampMs * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)?.let {
92
+ Bitmap.createScaledBitmap(it, width, height, true).also { scaled -> if (scaled !== it) it.recycle() }
93
+ }
94
+ }
95
+ return frame
96
+ }
97
+
98
+ private fun readDisk(key: String): Bitmap? {
99
+ val file = diskFile(key).takeIf { it.isFile } ?: return null
100
+ return BitmapFactory.decodeFile(file.absolutePath)?.also {
101
+ memory.put(key, it)
102
+ file.setLastModified(System.currentTimeMillis())
103
+ }
104
+ }
105
+
106
+ private fun writeDisk(key: String, bitmap: Bitmap) {
107
+ runCatching { diskFile(key).outputStream().buffered().use { bitmap.compress(Bitmap.CompressFormat.JPEG, 82, it) } }
108
+ }
109
+
110
+ private fun trimDisk() {
111
+ val files = cacheDir.listFiles()?.sortedBy { it.lastModified() } ?: return
112
+ var bytes = files.sumOf { it.length() }
113
+ for (file in files) {
114
+ if (bytes <= maxDiskBytes) break
115
+ bytes -= file.length()
116
+ file.delete()
117
+ }
118
+ }
119
+
120
+ private fun diskFile(key: String) = File(cacheDir, "$key.jpg")
121
+
122
+ fun clearMemory() = memory.evictAll()
123
+
124
+ fun close() {
125
+ requests.forEach(Request::cancel)
126
+ requests.clear()
127
+ executor.shutdownNow()
128
+ memory.evictAll()
129
+ }
130
+ }