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.
- package/CHANGELOG.md +27 -0
- package/CONTRIBUTING.md +101 -0
- package/LICENSE +20 -0
- package/PhotoVideoEditor.podspec +23 -0
- package/README.md +53 -0
- package/android/build.gradle +61 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/java/com/photovideoeditor/PhotoVideoEditorModule.kt +126 -0
- package/android/src/main/java/com/photovideoeditor/PhotoVideoEditorPackage.kt +31 -0
- package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +2521 -0
- package/android/src/main/java/com/photovideoeditor/editor/README.md +5 -0
- package/android/src/main/java/com/photovideoeditor/files/SourceResolver.kt +29 -0
- package/android/src/main/java/com/photovideoeditor/media/BitmapSampling.kt +11 -0
- package/android/src/main/java/com/photovideoeditor/model/EditorRequest.kt +3 -0
- package/android/src/main/java/com/photovideoeditor/photo/export/PhotoExporter.kt +201 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/OverlayGeometry.kt +22 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoAdjustmentRenderer.kt +160 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoAdjustments.kt +55 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoEditSession.kt +121 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoFilterPresets.kt +64 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayer.kt +54 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +108 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerStack.kt +60 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformState.kt +52 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +168 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/FilterThumbnailLoader.kt +57 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +202 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/PhotoPreviewRenderCoordinator.kt +57 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/ZoomableImageView.kt +96 -0
- package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +310 -0
- package/android/src/main/java/com/photovideoeditor/stickers/OpenMojiCatalog.kt +172 -0
- package/android/src/main/java/com/photovideoeditor/stickers/RemoteImageLoader.kt +110 -0
- package/android/src/main/java/com/photovideoeditor/ui/DesignTokens.kt +93 -0
- package/android/src/main/java/com/photovideoeditor/ui/components/EditorComponents.kt +119 -0
- package/android/src/main/java/com/photovideoeditor/validation/EditorRequestValidator.kt +11 -0
- package/android/src/main/java/com/photovideoeditor/video/cache/TimelineThumbnailRepository.kt +130 -0
- package/android/src/main/java/com/photovideoeditor/video/export/VideoExporter.kt +319 -0
- package/android/src/main/java/com/photovideoeditor/video/render/VideoClip.kt +20 -0
- package/android/src/main/java/com/photovideoeditor/video/render/VideoEditSession.kt +136 -0
- package/android/src/main/java/com/photovideoeditor/video/render/VideoThumbnailGenerator.kt +31 -0
- package/android/src/main/java/com/photovideoeditor/video/render/VideoTransformState.kt +48 -0
- package/android/src/main/java/com/photovideoeditor/video/ui/TrimRangeView.kt +104 -0
- package/android/src/main/res/drawable/ic_add.xml +9 -0
- package/android/src/main/res/drawable/ic_arrow_back.xml +9 -0
- package/android/src/main/res/drawable/ic_aspect_ratio.xml +9 -0
- package/android/src/main/res/drawable/ic_auto_fix_high.xml +9 -0
- package/android/src/main/res/drawable/ic_check.xml +9 -0
- package/android/src/main/res/drawable/ic_chevron_left.xml +9 -0
- package/android/src/main/res/drawable/ic_chevron_right.xml +9 -0
- package/android/src/main/res/drawable/ic_close.xml +9 -0
- package/android/src/main/res/drawable/ic_compare.xml +9 -0
- package/android/src/main/res/drawable/ic_content_copy.xml +9 -0
- package/android/src/main/res/drawable/ic_content_cut.xml +9 -0
- package/android/src/main/res/drawable/ic_crop.xml +9 -0
- package/android/src/main/res/drawable/ic_crop_square.xml +9 -0
- package/android/src/main/res/drawable/ic_delete.xml +9 -0
- package/android/src/main/res/drawable/ic_drag_indicator.xml +9 -0
- package/android/src/main/res/drawable/ic_face.xml +9 -0
- package/android/src/main/res/drawable/ic_filter_center_focus.xml +9 -0
- package/android/src/main/res/drawable/ic_flare.xml +9 -0
- package/android/src/main/res/drawable/ic_format_size.xml +9 -0
- package/android/src/main/res/drawable/ic_fullscreen.xml +9 -0
- package/android/src/main/res/drawable/ic_layers.xml +9 -0
- package/android/src/main/res/drawable/ic_light_mode.xml +9 -0
- package/android/src/main/res/drawable/ic_lock.xml +9 -0
- package/android/src/main/res/drawable/ic_magic_button.xml +9 -0
- package/android/src/main/res/drawable/ic_movie.xml +9 -0
- package/android/src/main/res/drawable/ic_opacity.xml +9 -0
- package/android/src/main/res/drawable/ic_palette.xml +9 -0
- package/android/src/main/res/drawable/ic_photo_filter.xml +9 -0
- package/android/src/main/res/drawable/ic_redo.xml +9 -0
- package/android/src/main/res/drawable/ic_rotate_90_degrees_ccw.xml +9 -0
- package/android/src/main/res/drawable/ic_rotate_left.xml +9 -0
- package/android/src/main/res/drawable/ic_rotate_right.xml +9 -0
- package/android/src/main/res/drawable/ic_sentiment_satisfied.xml +9 -0
- package/android/src/main/res/drawable/ic_share.xml +9 -0
- package/android/src/main/res/drawable/ic_speed.xml +9 -0
- package/android/src/main/res/drawable/ic_straighten.xml +9 -0
- package/android/src/main/res/drawable/ic_title.xml +9 -0
- package/android/src/main/res/drawable/ic_tune.xml +9 -0
- package/android/src/main/res/drawable/ic_undo.xml +9 -0
- package/android/src/main/res/drawable/ic_visibility_off.xml +9 -0
- package/android/src/main/res/drawable/ic_wallpaper.xml +9 -0
- package/android/src/main/res/values/styles.xml +10 -0
- package/docs/api.md +9 -0
- package/docs/architecture.md +9 -0
- package/docs/customization.md +18 -0
- package/docs/development.md +21 -0
- package/docs/formats.md +27 -0
- package/docs/performance.md +3 -0
- package/docs/permissions.md +3 -0
- package/docs/photo-editor.md +41 -0
- package/docs/roadmap.md +12 -0
- package/docs/technology-decisions.md +17 -0
- package/docs/video-editor.md +76 -0
- package/ios/Editor/DesignTokens.swift +75 -0
- package/ios/Editor/PhotoVideoEditorOutcome.swift +9 -0
- package/ios/Editor/PhotoVideoEditorViewController.swift +2064 -0
- package/ios/Editor/README.md +5 -0
- package/ios/Editor/UI/EditorComponents.swift +138 -0
- package/ios/Files/SourceResolver.swift +13 -0
- package/ios/Models/EditorRequest.swift +3 -0
- package/ios/Photo/Export/PhotoExporter.swift +144 -0
- package/ios/Photo/Rendering/OverlayGeometry.swift +22 -0
- package/ios/Photo/Rendering/PhotoAdjustmentRenderer.swift +141 -0
- package/ios/Photo/Rendering/PhotoAdjustments.swift +57 -0
- package/ios/Photo/Rendering/PhotoEditSession.swift +121 -0
- package/ios/Photo/Rendering/PhotoFilterPresets.swift +45 -0
- package/ios/Photo/Rendering/PhotoLayer.swift +59 -0
- package/ios/Photo/Rendering/PhotoLayerRenderer.swift +94 -0
- package/ios/Photo/Rendering/PhotoLayerStack.swift +60 -0
- package/ios/Photo/Rendering/PhotoTransformState.swift +49 -0
- package/ios/Photo/Rendering/PixelBuffer.swift +68 -0
- package/ios/Photo/UI/CropOverlayView.swift +161 -0
- package/ios/Photo/UI/LayerOverlayView.swift +176 -0
- package/ios/Photo/UI/ZoomableImageView.swift +79 -0
- package/ios/PhotoVideoEditor.mm +20 -0
- package/ios/PhotoVideoEditor.swift +70 -0
- package/ios/Stickers/OnlineStickerSheetViewController.swift +418 -0
- package/ios/Stickers/OpenMojiCatalog.swift +188 -0
- package/ios/Stickers/RemoteImageLoader.swift +127 -0
- package/ios/Validation/EditorRequestValidator.swift +9 -0
- package/ios/Video/Cache/TimelineThumbnailRepository.swift +148 -0
- package/ios/Video/Export/VideoExporter.swift +275 -0
- package/ios/Video/Render/VideoClip.swift +21 -0
- package/ios/Video/Render/VideoEditSession.swift +99 -0
- package/ios/Video/Render/VideoThumbnailGenerator.swift +25 -0
- package/ios/Video/Render/VideoTransformState.swift +23 -0
- package/ios/Video/UI/TrimRangeView.swift +91 -0
- package/lib/module/NativePhotoVideoEditor.js +5 -0
- package/lib/module/NativePhotoVideoEditor.js.map +1 -0
- package/lib/module/errors.js +29 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/events.js +27 -0
- package/lib/module/events.js.map +1 -0
- package/lib/module/index.js +91 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/project.js +4 -0
- package/lib/module/project.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativePhotoVideoEditor.d.ts +11 -0
- package/lib/typescript/src/NativePhotoVideoEditor.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +11 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/events.d.ts +5 -0
- package/lib/typescript/src/events.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +15 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/project.d.ts +24 -0
- package/lib/typescript/src/project.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +108 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +154 -0
- package/src/NativePhotoVideoEditor.ts +12 -0
- package/src/errors.ts +78 -0
- package/src/events.ts +43 -0
- package/src/index.ts +123 -0
- package/src/project.ts +17 -0
- 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
|
+
}
|