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.
- package/CHANGELOG.md +20 -0
- package/PhotoVideoEditor.podspec +1 -0
- package/README.md +10 -6
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/android/build.gradle +1 -1
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/assets/Stickers/AUTUMN_1.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_10.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_11.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_12.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_13.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_14.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_15.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_16.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_17.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_18.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_2.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_3.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_4.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_5.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_6.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_7.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_8.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_9.png +0 -0
- package/android/src/main/assets/Stickers/LOL_1.png +0 -0
- package/android/src/main/assets/Stickers/LOL_10.png +0 -0
- package/android/src/main/assets/Stickers/LOL_11.png +0 -0
- package/android/src/main/assets/Stickers/LOL_12.png +0 -0
- package/android/src/main/assets/Stickers/LOL_13.png +0 -0
- package/android/src/main/assets/Stickers/LOL_14.png +0 -0
- package/android/src/main/assets/Stickers/LOL_15.png +0 -0
- package/android/src/main/assets/Stickers/LOL_16.png +0 -0
- package/android/src/main/assets/Stickers/LOL_17.png +0 -0
- package/android/src/main/assets/Stickers/LOL_18.png +0 -0
- package/android/src/main/assets/Stickers/LOL_19.png +0 -0
- package/android/src/main/assets/Stickers/LOL_2.png +0 -0
- package/android/src/main/assets/Stickers/LOL_3.png +0 -0
- package/android/src/main/assets/Stickers/LOL_4.png +0 -0
- package/android/src/main/assets/Stickers/LOL_5.png +0 -0
- package/android/src/main/assets/Stickers/LOL_6.png +0 -0
- package/android/src/main/assets/Stickers/LOL_7.png +0 -0
- package/android/src/main/assets/Stickers/LOL_8.png +0 -0
- package/android/src/main/assets/Stickers/LOL_9.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_1.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_2.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_3.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_4.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_5.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_6.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_1.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_10.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_11.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_12.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_13.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_14.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_15.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_16.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_17.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_18.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_19.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_2.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_20.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_21.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_3.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_4.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_5.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_6.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_7.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_8.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_9.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_1.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_12.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_13.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_14.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_15.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_16.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_2.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_25.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_26.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_27.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_28.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_29.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_3.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_30.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_31.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_32.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_7.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_8.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_9.png +0 -0
- package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +901 -651
- package/android/src/main/java/com/photovideoeditor/photo/export/PhotoExporter.kt +8 -23
- package/android/src/main/java/com/photovideoeditor/photo/render/CropGeometry.kt +71 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/OverlayGeometry.kt +36 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoEditSession.kt +5 -7
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoFilterPresets.kt +71 -41
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayer.kt +5 -1
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +24 -1
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformRenderer.kt +33 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformState.kt +6 -1
- package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +112 -122
- package/android/src/main/java/com/photovideoeditor/photo/ui/DrawOverlayView.kt +66 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +63 -14
- package/android/src/main/java/com/photovideoeditor/photo/ui/ZoomableImageView.kt +10 -0
- package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +135 -104
- package/android/src/main/java/com/photovideoeditor/ui/components/CropPanel.kt +122 -0
- package/android/src/main/java/com/photovideoeditor/ui/components/EditorPanel.kt +250 -0
- package/android/src/main/java/com/photovideoeditor/ui/components/EditorToolSlider.kt +103 -0
- package/android/src/main/java/com/photovideoeditor/video/export/VideoExporter.kt +38 -11
- package/android/src/main/java/com/photovideoeditor/video/render/VideoCropEffects.kt +35 -0
- package/android/src/main/java/com/photovideoeditor/video/render/VideoEditSession.kt +10 -0
- package/android/src/main/java/com/photovideoeditor/video/render/VideoTransformState.kt +1 -0
- package/android/src/main/res/drawable/ic_draw.xml +6 -0
- package/android/src/main/res/drawable/ic_pause.xml +7 -0
- package/android/src/main/res/drawable/ic_play.xml +7 -0
- package/docs/api.md +1 -1
- package/docs/editor-ui-validation.md +22 -0
- package/docs/photo-editor.md +5 -6
- package/ios/Editor/PhotoVideoEditorViewController.swift +918 -494
- package/ios/Editor/UI/CropPanel.swift +97 -0
- package/ios/Editor/UI/EditorPanel.swift +334 -0
- package/ios/Editor/UI/EditorToolSlider.swift +22 -0
- package/ios/Editor/UI/TextEditorSheet.swift +84 -0
- package/ios/Photo/Export/PhotoExporter.swift +3 -3
- package/ios/Photo/Rendering/CropGeometry.swift +48 -0
- package/ios/Photo/Rendering/PhotoEditSession.swift +21 -10
- package/ios/Photo/Rendering/PhotoFilterPresets.swift +53 -23
- package/ios/Photo/Rendering/PhotoLayer.swift +14 -2
- package/ios/Photo/Rendering/PhotoLayerRenderer.swift +16 -1
- package/ios/Photo/Rendering/PhotoTransformState.swift +5 -0
- package/ios/Photo/UI/CropOverlayView.swift +98 -134
- package/ios/Photo/UI/DrawOverlayView.swift +48 -0
- package/ios/Photo/UI/FilterThumbnailLoader.swift +62 -0
- package/ios/Photo/UI/LayerOverlayView.swift +36 -11
- package/ios/Photo/UI/ZoomableImageView.swift +19 -1
- package/ios/Resources/Stickers/AUTUMN_1.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_10.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_11.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_12.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_13.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_14.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_15.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_16.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_17.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_18.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_2.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_3.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_4.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_5.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_6.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_7.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_8.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_9.png +0 -0
- package/ios/Resources/Stickers/LOL_1.png +0 -0
- package/ios/Resources/Stickers/LOL_10.png +0 -0
- package/ios/Resources/Stickers/LOL_11.png +0 -0
- package/ios/Resources/Stickers/LOL_12.png +0 -0
- package/ios/Resources/Stickers/LOL_13.png +0 -0
- package/ios/Resources/Stickers/LOL_14.png +0 -0
- package/ios/Resources/Stickers/LOL_15.png +0 -0
- package/ios/Resources/Stickers/LOL_16.png +0 -0
- package/ios/Resources/Stickers/LOL_17.png +0 -0
- package/ios/Resources/Stickers/LOL_18.png +0 -0
- package/ios/Resources/Stickers/LOL_19.png +0 -0
- package/ios/Resources/Stickers/LOL_2.png +0 -0
- package/ios/Resources/Stickers/LOL_3.png +0 -0
- package/ios/Resources/Stickers/LOL_4.png +0 -0
- package/ios/Resources/Stickers/LOL_5.png +0 -0
- package/ios/Resources/Stickers/LOL_6.png +0 -0
- package/ios/Resources/Stickers/LOL_7.png +0 -0
- package/ios/Resources/Stickers/LOL_8.png +0 -0
- package/ios/Resources/Stickers/LOL_9.png +0 -0
- package/ios/Resources/Stickers/SPRING_1.png +0 -0
- package/ios/Resources/Stickers/SPRING_2.png +0 -0
- package/ios/Resources/Stickers/SPRING_3.png +0 -0
- package/ios/Resources/Stickers/SPRING_4.png +0 -0
- package/ios/Resources/Stickers/SPRING_5.png +0 -0
- package/ios/Resources/Stickers/SPRING_6.png +0 -0
- package/ios/Resources/Stickers/SUMMER_1.png +0 -0
- package/ios/Resources/Stickers/SUMMER_10.png +0 -0
- package/ios/Resources/Stickers/SUMMER_11.png +0 -0
- package/ios/Resources/Stickers/SUMMER_12.png +0 -0
- package/ios/Resources/Stickers/SUMMER_13.png +0 -0
- package/ios/Resources/Stickers/SUMMER_14.png +0 -0
- package/ios/Resources/Stickers/SUMMER_15.png +0 -0
- package/ios/Resources/Stickers/SUMMER_16.png +0 -0
- package/ios/Resources/Stickers/SUMMER_17.png +0 -0
- package/ios/Resources/Stickers/SUMMER_18.png +0 -0
- package/ios/Resources/Stickers/SUMMER_19.png +0 -0
- package/ios/Resources/Stickers/SUMMER_2.png +0 -0
- package/ios/Resources/Stickers/SUMMER_20.png +0 -0
- package/ios/Resources/Stickers/SUMMER_21.png +0 -0
- package/ios/Resources/Stickers/SUMMER_3.png +0 -0
- package/ios/Resources/Stickers/SUMMER_4.png +0 -0
- package/ios/Resources/Stickers/SUMMER_5.png +0 -0
- package/ios/Resources/Stickers/SUMMER_6.png +0 -0
- package/ios/Resources/Stickers/SUMMER_7.png +0 -0
- package/ios/Resources/Stickers/SUMMER_8.png +0 -0
- package/ios/Resources/Stickers/SUMMER_9.png +0 -0
- package/ios/Resources/Stickers/TEXT_1.png +0 -0
- package/ios/Resources/Stickers/TEXT_12.png +0 -0
- package/ios/Resources/Stickers/TEXT_13.png +0 -0
- package/ios/Resources/Stickers/TEXT_14.png +0 -0
- package/ios/Resources/Stickers/TEXT_15.png +0 -0
- package/ios/Resources/Stickers/TEXT_16.png +0 -0
- package/ios/Resources/Stickers/TEXT_2.png +0 -0
- package/ios/Resources/Stickers/TEXT_25.png +0 -0
- package/ios/Resources/Stickers/TEXT_26.png +0 -0
- package/ios/Resources/Stickers/TEXT_27.png +0 -0
- package/ios/Resources/Stickers/TEXT_28.png +0 -0
- package/ios/Resources/Stickers/TEXT_29.png +0 -0
- package/ios/Resources/Stickers/TEXT_3.png +0 -0
- package/ios/Resources/Stickers/TEXT_30.png +0 -0
- package/ios/Resources/Stickers/TEXT_31.png +0 -0
- package/ios/Resources/Stickers/TEXT_32.png +0 -0
- package/ios/Resources/Stickers/TEXT_7.png +0 -0
- package/ios/Resources/Stickers/TEXT_8.png +0 -0
- package/ios/Resources/Stickers/TEXT_9.png +0 -0
- package/ios/Resources/Stickers/a_pety_icon.png +0 -0
- package/ios/Stickers/OnlineStickerSheetViewController.swift +102 -159
- package/ios/Video/Export/VideoExporter.swift +10 -1
- package/ios/Video/Render/VideoCropGeometry.swift +48 -0
- package/ios/Video/Render/VideoTransformState.swift +1 -0
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/types.d.ts +2 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +1 -1
- package/src/types.ts +2 -2
- package/android/src/main/java/com/photovideoeditor/stickers/OpenMojiCatalog.kt +0 -172
- package/android/src/main/java/com/photovideoeditor/stickers/RemoteImageLoader.kt +0 -110
- package/ios/Stickers/OpenMojiCatalog.swift +0 -188
- package/ios/Stickers/RemoteImageLoader.swift +0 -127
|
@@ -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
|
-
}
|