@iternio/react-native-auto-play 0.3.11 → 0.3.13
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/README.md +103 -3
- package/ReactNativeAutoPlay.podspec +0 -4
- package/android/src/automotive/AndroidManifest.xml +1 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlay.kt +91 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VoiceInputManager.kt +214 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/MapTemplate.kt +2 -5
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +117 -38
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/BitmapCache.kt +14 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/SymbolFont.kt +29 -30
- package/ios/extensions/NitroImageExtensions.swift +10 -1
- package/ios/hybrid/HybridAutoPlay.swift +51 -4
- package/ios/templates/GridTemplate.swift +7 -0
- package/ios/templates/MapTemplate.swift +14 -0
- package/ios/templates/Parser.swift +91 -4
- package/ios/utils/SymbolFont.swift +44 -44
- package/ios/utils/VoiceInputManager.swift +233 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/specs/AutoPlay.nitro.d.ts +31 -1
- package/lib/templates/MapTemplate.d.ts +4 -1
- package/lib/types/Image.d.ts +46 -4
- package/lib/types/Maneuver.d.ts +2 -10
- package/lib/utils/NitroImage.d.ts +29 -3
- package/lib/utils/NitroImage.js +64 -3
- package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -1
- package/nitrogen/generated/android/c++/JGlyphImage.hpp +6 -1
- package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.cpp +48 -1
- package/nitrogen/generated/android/c++/JHybridAutoPlaySpec.hpp +4 -0
- package/nitrogen/generated/android/c++/JHybridClusterSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridMessageTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JHybridSignInTemplateSpec.cpp +5 -1
- package/nitrogen/generated/android/c++/JImageLane.hpp +2 -0
- package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JLaneGuidance.hpp +2 -0
- package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JMapTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JMessageTemplateConfig.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroAction.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroAttributedString.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroAttributedStringImage.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroBaseMapTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JNitroGridButton.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroImage.cpp +6 -2
- package/nitrogen/generated/android/c++/JNitroImage.hpp +20 -3
- package/nitrogen/generated/android/c++/JNitroManeuver.hpp +3 -1
- package/nitrogen/generated/android/c++/JNitroMapButton.hpp +2 -0
- package/nitrogen/generated/android/c++/JNitroMessageManeuver.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroNavigationAlert.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroRoutingManeuver.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroRow.hpp +7 -5
- package/nitrogen/generated/android/c++/JNitroSection.hpp +3 -1
- package/nitrogen/generated/android/c++/JPreferredImageLane.hpp +2 -0
- package/nitrogen/generated/android/c++/JRemoteImage.hpp +68 -0
- package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JSignInTemplateConfig.hpp +3 -1
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.cpp +30 -0
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage_RemoteImage.hpp +92 -0
- package/nitrogen/generated/android/c++/JVariant_PreferredImageLane_ImageLane.hpp +3 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GlyphImage.kt +5 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridAutoPlaySpec.kt +17 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/MessageTemplateConfig.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroAction.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroImage.kt +14 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroMessageManeuver.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroNavigationAlert.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRoutingManeuver.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +3 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/RemoteImage.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/{Variant_GlyphImage_AssetImage.kt → Variant_GlyphImage_AssetImage_RemoteImage.kt} +20 -8
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.cpp +16 -8
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +156 -79
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +4 -0
- package/nitrogen/generated/ios/c++/HybridAutoPlaySpecSwift.hpp +37 -0
- package/nitrogen/generated/ios/c++/HybridCarPlayDashboardSpecSwift.hpp +4 -1
- package/nitrogen/generated/ios/c++/HybridClusterSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridMessageTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
- package/nitrogen/generated/ios/swift/GlyphImage.swift +7 -2
- package/nitrogen/generated/ios/swift/HybridAutoPlaySpec.swift +4 -0
- package/nitrogen/generated/ios/swift/HybridAutoPlaySpec_cxx.swift +82 -0
- package/nitrogen/generated/ios/swift/ImageLane.swift +9 -4
- package/nitrogen/generated/ios/swift/MessageTemplateConfig.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroAction.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroAttributedStringImage.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroCarPlayDashboardButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroGridButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroImage.swift +2 -1
- package/nitrogen/generated/ios/swift/NitroMapButton.swift +9 -4
- package/nitrogen/generated/ios/swift/NitroMessageManeuver.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroNavigationAlert.swift +16 -11
- package/nitrogen/generated/ios/swift/NitroRoutingManeuver.swift +25 -15
- package/nitrogen/generated/ios/swift/NitroRow.swift +16 -11
- package/nitrogen/generated/ios/swift/PreferredImageLane.swift +9 -4
- package/nitrogen/generated/ios/swift/RemoteImage.swift +58 -0
- package/nitrogen/generated/ios/swift/{Variant_GlyphImage_AssetImage.swift → Variant_GlyphImage_AssetImage_RemoteImage.swift} +4 -3
- package/nitrogen/generated/shared/c++/GlyphImage.hpp +6 -1
- package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridAutoPlaySpec.hpp +5 -0
- package/nitrogen/generated/shared/c++/ImageLane.hpp +8 -5
- package/nitrogen/generated/shared/c++/MessageTemplateConfig.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroAction.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroAttributedStringImage.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroCarPlayDashboardButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroGridButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroMapButton.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroMessageManeuver.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroNavigationAlert.hpp +8 -5
- package/nitrogen/generated/shared/c++/NitroRoutingManeuver.hpp +12 -9
- package/nitrogen/generated/shared/c++/NitroRow.hpp +8 -5
- package/nitrogen/generated/shared/c++/PreferredImageLane.hpp +8 -5
- package/nitrogen/generated/shared/c++/RemoteImage.hpp +94 -0
- package/package.json +2 -3
- package/src/index.ts +1 -0
- package/src/specs/AutoPlay.nitro.ts +39 -1
- package/src/templates/MapTemplate.ts +4 -1
- package/src/types/Image.ts +65 -16
- package/src/types/Maneuver.ts +3 -10
- package/src/utils/NitroImage.ts +81 -6
- package/android/src/main/res/font/materialsymbolsoutlined_regular.ttf +0 -0
- package/ios/Assets/MaterialSymbolsOutlined-Regular.ttf +0 -0
- package/lib/types/Glyphmap.d.ts +0 -4105
- package/lib/types/Glyphmap.js +0 -4105
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.cpp +0 -26
- package/nitrogen/generated/android/c++/JVariant_GlyphImage_AssetImage.hpp +0 -75
- package/src/types/Glyphmap.ts +0 -4107
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt
CHANGED
|
@@ -40,6 +40,7 @@ import com.facebook.datasource.DataSources
|
|
|
40
40
|
import com.facebook.drawee.backends.pipeline.Fresco
|
|
41
41
|
import com.facebook.imagepipeline.image.CloseableBitmap
|
|
42
42
|
import com.facebook.imagepipeline.image.CloseableXml
|
|
43
|
+
import com.facebook.imagepipeline.request.ImageRequest
|
|
43
44
|
import com.facebook.imagepipeline.request.ImageRequestBuilder
|
|
44
45
|
import com.facebook.react.views.imagehelper.ImageSource
|
|
45
46
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.AndroidAutoScreen
|
|
@@ -67,10 +68,11 @@ import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroRow
|
|
|
67
68
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroSectionType
|
|
68
69
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.OffRampType
|
|
69
70
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.OnRampType
|
|
71
|
+
import com.margelo.nitro.swe.iternio.reactnativeautoplay.RemoteImage
|
|
70
72
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.TrafficSide
|
|
71
73
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.TravelEstimates
|
|
72
74
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.TurnType
|
|
73
|
-
import com.margelo.nitro.swe.iternio.reactnativeautoplay.
|
|
75
|
+
import com.margelo.nitro.swe.iternio.reactnativeautoplay.Variant_GlyphImage_AssetImage_RemoteImage
|
|
74
76
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.BitmapCache
|
|
75
77
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.SymbolFont
|
|
76
78
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.utils.get
|
|
@@ -79,6 +81,7 @@ import java.util.Calendar
|
|
|
79
81
|
import java.util.Locale
|
|
80
82
|
import java.util.TimeZone
|
|
81
83
|
import kotlin.math.abs
|
|
84
|
+
import androidx.core.net.toUri
|
|
82
85
|
|
|
83
86
|
object Parser {
|
|
84
87
|
const val TAG = "Parser"
|
|
@@ -210,27 +213,35 @@ object Parser {
|
|
|
210
213
|
}.build()
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
fun parseImage(context: CarContext, image:
|
|
214
|
-
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull())
|
|
216
|
+
fun parseImage(context: CarContext, image: Variant_GlyphImage_AssetImage_RemoteImage): CarIcon {
|
|
217
|
+
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull(), image.asThirdOrNull())
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
fun parseImage(context: CarContext, image: NitroImage): CarIcon {
|
|
218
|
-
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull())
|
|
221
|
+
return parseImage(context, image.asFirstOrNull(), image.asSecondOrNull(), image.asThirdOrNull())
|
|
219
222
|
}
|
|
220
223
|
|
|
221
|
-
fun parseImage(
|
|
222
|
-
|
|
224
|
+
fun parseImage(
|
|
225
|
+
context: CarContext,
|
|
226
|
+
glyphImage: GlyphImage?,
|
|
227
|
+
assetImage: AssetImage?,
|
|
228
|
+
remoteImage: RemoteImage?
|
|
229
|
+
): CarIcon {
|
|
230
|
+
val bitmap = parseImageToBitmap(context, glyphImage, assetImage, remoteImage)
|
|
223
231
|
|
|
224
232
|
bitmap?.let {
|
|
225
233
|
return CarIcon.Builder(IconCompat.createWithBitmap(it)).build()
|
|
226
234
|
}
|
|
227
235
|
|
|
228
|
-
//
|
|
229
|
-
return CarIcon.
|
|
236
|
+
// remote images might fail to load so we provide some placeholder then
|
|
237
|
+
return CarIcon.ALERT
|
|
230
238
|
}
|
|
231
239
|
|
|
232
240
|
fun parseImageToBitmap(
|
|
233
|
-
context: CarContext,
|
|
241
|
+
context: CarContext,
|
|
242
|
+
glyphImage: GlyphImage?,
|
|
243
|
+
assetImage: AssetImage?,
|
|
244
|
+
remoteImage: RemoteImage?
|
|
234
245
|
): Bitmap? {
|
|
235
246
|
glyphImage?.let {
|
|
236
247
|
return SymbolFont.imageFromNitroImage(
|
|
@@ -240,6 +251,9 @@ object Parser {
|
|
|
240
251
|
assetImage?.let {
|
|
241
252
|
return parseAssetImage(context, it)
|
|
242
253
|
}
|
|
254
|
+
remoteImage?.let {
|
|
255
|
+
return parseRemoteImage(context, it)
|
|
256
|
+
}
|
|
243
257
|
|
|
244
258
|
return null
|
|
245
259
|
}
|
|
@@ -251,10 +265,14 @@ object Parser {
|
|
|
251
265
|
fun imageFromNitroImages(
|
|
252
266
|
context: CarContext, images: List<NitroImage>
|
|
253
267
|
): IconCompat {
|
|
254
|
-
val bitmaps = images.
|
|
268
|
+
val bitmaps = images.mapNotNull {
|
|
255
269
|
parseImageToBitmap(
|
|
256
|
-
context, it.asFirstOrNull(), it.asSecondOrNull()
|
|
257
|
-
)
|
|
270
|
+
context, it.asFirstOrNull(), it.asSecondOrNull(), it.asThirdOrNull()
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (bitmaps.isEmpty()) {
|
|
275
|
+
return IconCompat.createWithBitmap(createBitmap(1, 1))
|
|
258
276
|
}
|
|
259
277
|
|
|
260
278
|
val height = bitmaps.maxOf { it.height }
|
|
@@ -512,54 +530,115 @@ object Parser {
|
|
|
512
530
|
}
|
|
513
531
|
|
|
514
532
|
fun parseAssetImage(context: CarContext, assetImage: AssetImage): Bitmap? {
|
|
515
|
-
|
|
533
|
+
BitmapCache.get(context, assetImage)?.let { return it }
|
|
516
534
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
535
|
+
val imageRequest = buildImageRequest(ImageSource(context, assetImage.uri).uri)
|
|
536
|
+
val bitmap = fetchBitmap(context, imageRequest, timeoutMs = null) ?: return null
|
|
520
537
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
.
|
|
538
|
+
return applyTintAndCache(
|
|
539
|
+
context = context,
|
|
540
|
+
color = assetImage.color,
|
|
541
|
+
bitmap = bitmap,
|
|
542
|
+
cache = { result -> BitmapCache.put(context, assetImage, result) }
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
fun parseRemoteImage(context: CarContext, remoteImage: RemoteImage): Bitmap? {
|
|
547
|
+
BitmapCache.get(context, remoteImage)?.let { return it }
|
|
524
548
|
|
|
525
|
-
val
|
|
526
|
-
val
|
|
549
|
+
val imageRequest = buildImageRequest(remoteImage.uri.toUri())
|
|
550
|
+
val timeoutMs = remoteImage.timeoutMs?.toLong() ?: 500L
|
|
551
|
+
val bitmap = fetchBitmap(context, imageRequest, timeoutMs = timeoutMs) ?: return null
|
|
527
552
|
|
|
553
|
+
return applyTintAndCache(
|
|
554
|
+
context = context,
|
|
555
|
+
color = remoteImage.color,
|
|
556
|
+
bitmap = bitmap,
|
|
557
|
+
cache = { result -> BitmapCache.put(context, remoteImage, result) }
|
|
558
|
+
)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Disable Fresco's own caching — BitmapCache handles all caching uniformly
|
|
562
|
+
private fun buildImageRequest(uri: android.net.Uri): ImageRequest =
|
|
563
|
+
ImageRequestBuilder.newBuilderWithSource(uri)
|
|
564
|
+
.disableDiskCache().disableMemoryCache().build()
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Fetches a bitmap via Fresco's image pipeline.
|
|
568
|
+
* When [timeoutMs] is provided, the fetch runs on a background thread and is abandoned on timeout.
|
|
569
|
+
* When `null`, the fetch runs synchronously (bundled assets, no network I/O).
|
|
570
|
+
*/
|
|
571
|
+
private fun fetchBitmap(
|
|
572
|
+
context: CarContext,
|
|
573
|
+
imageRequest: ImageRequest,
|
|
574
|
+
timeoutMs: Long?
|
|
575
|
+
): Bitmap? {
|
|
576
|
+
if (timeoutMs == null) return fetchSync(context, imageRequest)
|
|
577
|
+
|
|
578
|
+
var fetched: Bitmap? = null
|
|
579
|
+
val thread = Thread { fetched = fetchSync(context, imageRequest) }
|
|
580
|
+
thread.start()
|
|
581
|
+
thread.join(timeoutMs)
|
|
582
|
+
if (thread.isAlive) {
|
|
583
|
+
// Fresco's waitForFinalResult is not interruptible, but setting the flag
|
|
584
|
+
// lets any subsequent interruptible call exit and signals intent to exit.
|
|
585
|
+
thread.interrupt()
|
|
586
|
+
return null
|
|
587
|
+
}
|
|
588
|
+
return fetched
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private fun fetchSync(context: CarContext, imageRequest: ImageRequest): Bitmap? {
|
|
592
|
+
val dataSource = try {
|
|
593
|
+
Fresco.getImagePipeline().fetchDecodedImage(imageRequest, context)
|
|
594
|
+
} catch (_: Exception) { return null }
|
|
595
|
+
val result = try {
|
|
596
|
+
DataSources.waitForFinalResult(dataSource)
|
|
597
|
+
} catch (_: Exception) { dataSource.close(); return null }
|
|
528
598
|
val image = result?.get()
|
|
529
599
|
try {
|
|
530
600
|
if (image is CloseableBitmap) {
|
|
531
|
-
|
|
601
|
+
// underlyingBitmap can be null when Fresco decodes to a CloseableBitmap
|
|
602
|
+
// whose backing bitmap has already been recycled or failed to allocate;
|
|
603
|
+
// copy() can also throw (e.g., OOM on very large remote images). Either
|
|
604
|
+
// way we return null so the caller falls back to a placeholder icon.
|
|
605
|
+
return image.underlyingBitmap?.copy(Bitmap.Config.ARGB_8888, false)
|
|
532
606
|
} else if (image is CloseableXml) {
|
|
533
607
|
val drawable = image.buildDrawable()
|
|
534
|
-
|
|
535
|
-
width = image.width, height = image.height, Bitmap.Config.ARGB_8888
|
|
536
|
-
)
|
|
608
|
+
return drawable?.toBitmap(width = image.width, height = image.height, Bitmap.Config.ARGB_8888)
|
|
537
609
|
}
|
|
610
|
+
} catch (_: Exception) {
|
|
611
|
+
// Any decode/copy failure (OOM, recycled bitmap, invalid config, …) should
|
|
612
|
+
// not crash the car app — the image is optional decoration and the caller
|
|
613
|
+
// handles a null return with CarIcon.ALERT.
|
|
614
|
+
return null
|
|
538
615
|
} finally {
|
|
539
616
|
image?.close()
|
|
540
617
|
result?.close()
|
|
541
618
|
dataSource.close()
|
|
542
619
|
}
|
|
620
|
+
return null
|
|
621
|
+
}
|
|
543
622
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
623
|
+
private fun applyTintAndCache(
|
|
624
|
+
context: CarContext,
|
|
625
|
+
color: NitroColor?,
|
|
626
|
+
bitmap: Bitmap,
|
|
627
|
+
cache: (Bitmap) -> Unit
|
|
628
|
+
): Bitmap {
|
|
629
|
+
color?.get(context)?.let { tint ->
|
|
630
|
+
val tinted = createBitmap(bitmap.width, bitmap.height)
|
|
631
|
+
val canvas = Canvas(tinted)
|
|
551
632
|
val paint = Paint()
|
|
552
633
|
|
|
553
|
-
paint.colorFilter = PorterDuffColorFilter(
|
|
634
|
+
paint.colorFilter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_IN)
|
|
554
635
|
canvas.drawBitmap(bitmap, 0f, 0f, paint)
|
|
555
636
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return result
|
|
637
|
+
cache(tinted)
|
|
638
|
+
return tinted
|
|
559
639
|
}
|
|
560
640
|
|
|
561
|
-
|
|
562
|
-
|
|
641
|
+
cache(bitmap)
|
|
563
642
|
return bitmap
|
|
564
643
|
}
|
|
565
644
|
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/BitmapCache.kt
CHANGED
|
@@ -6,6 +6,7 @@ import androidx.car.app.CarContext
|
|
|
6
6
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.AssetImage
|
|
7
7
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.GlyphImage
|
|
8
8
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroColor
|
|
9
|
+
import com.margelo.nitro.swe.iternio.reactnativeautoplay.RemoteImage
|
|
9
10
|
|
|
10
11
|
object BitmapCache {
|
|
11
12
|
private val maxMemory = Runtime.getRuntime().maxMemory()
|
|
@@ -37,6 +38,16 @@ object BitmapCache {
|
|
|
37
38
|
put(key, bitmap)
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
fun get(context: CarContext, image: RemoteImage): Bitmap? {
|
|
42
|
+
val key = image.cacheKey(context)
|
|
43
|
+
return get(key)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun put(context: CarContext, image: RemoteImage, bitmap: Bitmap) {
|
|
47
|
+
val key = image.cacheKey(context)
|
|
48
|
+
put(key, bitmap)
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
private fun get(key: String): Bitmap? {
|
|
41
52
|
synchronized(bitmapCache) {
|
|
42
53
|
return bitmapCache.get(key)
|
|
@@ -56,5 +67,8 @@ fun NitroColor.get(context: CarContext): Int =
|
|
|
56
67
|
fun AssetImage.cacheKey(context: CarContext): String =
|
|
57
68
|
this.color?.let { "${this.uri}/${it.get(context)}" } ?: run { this.uri }
|
|
58
69
|
|
|
70
|
+
fun RemoteImage.cacheKey(context: CarContext): String =
|
|
71
|
+
this.color?.let { "${this.uri}/${it.get(context)}" } ?: run { this.uri }
|
|
72
|
+
|
|
59
73
|
fun GlyphImage.cacheKey(context: CarContext): String =
|
|
60
74
|
"$glyph/${color.get(context)}/${backgroundColor.get(context)}/$fontScale"
|
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/utils/SymbolFont.kt
CHANGED
|
@@ -10,39 +10,36 @@ import android.graphics.Typeface
|
|
|
10
10
|
import androidx.car.app.CarContext
|
|
11
11
|
import androidx.core.content.res.ResourcesCompat
|
|
12
12
|
import androidx.core.graphics.createBitmap
|
|
13
|
-
import androidx.core.graphics.drawable.IconCompat
|
|
14
|
-
import com.margelo.nitro.swe.iternio.reactnativeautoplay.GlyphImage
|
|
15
|
-
import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroImage
|
|
16
13
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.BuildConfig
|
|
17
|
-
import com.margelo.nitro.swe.iternio.reactnativeautoplay.
|
|
18
|
-
import com.margelo.nitro.swe.iternio.reactnativeautoplay.template.Parser
|
|
14
|
+
import com.margelo.nitro.swe.iternio.reactnativeautoplay.GlyphImage
|
|
19
15
|
|
|
20
16
|
object SymbolFont {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
private var typeface: Typeface? = null
|
|
24
|
-
|
|
25
|
-
private fun loadFont(context: Context) {
|
|
26
|
-
if (typeface != null) {
|
|
27
|
-
return
|
|
28
|
-
}
|
|
17
|
+
private var cachedFontName: String? = null
|
|
18
|
+
private var cachedTypeface: Typeface? = null
|
|
29
19
|
|
|
30
|
-
|
|
20
|
+
private fun loadTypeface(context: Context, fontName: String): Typeface? {
|
|
21
|
+
if (fontName == cachedFontName) return cachedTypeface
|
|
22
|
+
val id = context.resources.getIdentifier(
|
|
23
|
+
fontName.lowercase(), "font", context.packageName
|
|
24
|
+
)
|
|
25
|
+
if (id == 0) return null
|
|
26
|
+
val tf = ResourcesCompat.getFont(context, id) ?: return null
|
|
27
|
+
cachedFontName = fontName
|
|
28
|
+
cachedTypeface = tf
|
|
29
|
+
return tf
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
private fun imageFromGlyph(
|
|
34
33
|
context: Context,
|
|
35
|
-
|
|
34
|
+
glyphImage: GlyphImage,
|
|
36
35
|
color: Int,
|
|
37
36
|
backgroundColor: Int,
|
|
38
37
|
cornerRadius: Float = 8f, //TODO: make accessible and add it to GlyphImage.cacheKey
|
|
39
|
-
fontScale: Float,
|
|
40
38
|
): Bitmap? {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
39
|
+
val font =
|
|
40
|
+
loadTypeface(context, glyphImage.fontName) ?: run {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
46
43
|
|
|
47
44
|
val virtualScreenDensity = context.resources.displayMetrics.density
|
|
48
45
|
val scale = BuildConfig.SCALE_FACTOR * virtualScreenDensity
|
|
@@ -59,6 +56,8 @@ object SymbolFont {
|
|
|
59
56
|
}
|
|
60
57
|
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint)
|
|
61
58
|
|
|
59
|
+
val fontScale = (glyphImage.fontScale ?: 1.0).toFloat()
|
|
60
|
+
|
|
62
61
|
// Setup text paint
|
|
63
62
|
paint.reset()
|
|
64
63
|
paint = Paint().apply {
|
|
@@ -70,7 +69,7 @@ object SymbolFont {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
// Get the character from codepoint
|
|
73
|
-
val codepoint = glyph.toInt()
|
|
72
|
+
val codepoint = glyphImage.glyph.toInt()
|
|
74
73
|
val text = String(Character.toChars(codepoint))
|
|
75
74
|
|
|
76
75
|
// Measure text
|
|
@@ -94,13 +93,13 @@ object SymbolFont {
|
|
|
94
93
|
return bitmap
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
bitmap =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
bitmap =
|
|
97
|
+
imageFromGlyph(
|
|
98
|
+
context = context,
|
|
99
|
+
glyphImage = image,
|
|
100
|
+
color = image.color.get(context),
|
|
101
|
+
backgroundColor = image.backgroundColor.get(context),
|
|
102
|
+
)
|
|
104
103
|
|
|
105
104
|
bitmap?.let {
|
|
106
105
|
BitmapCache.put(context, image, it)
|
|
@@ -108,4 +107,4 @@ object SymbolFont {
|
|
|
108
107
|
|
|
109
108
|
return bitmap
|
|
110
109
|
}
|
|
111
|
-
}
|
|
110
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
protocol ImageProtocol {
|
|
9
9
|
var glyphImage: GlyphImage? { get }
|
|
10
10
|
var assetImage: AssetImage? { get }
|
|
11
|
+
var remoteImage: RemoteImage? { get }
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
extension NitroImage: ImageProtocol {
|
|
@@ -19,9 +20,13 @@ extension NitroImage: ImageProtocol {
|
|
|
19
20
|
if case .second(let asset) = self { return asset }
|
|
20
21
|
return nil
|
|
21
22
|
}
|
|
23
|
+
var remoteImage: RemoteImage? {
|
|
24
|
+
if case .third(let remote) = self { return remote }
|
|
25
|
+
return nil
|
|
26
|
+
}
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
extension
|
|
29
|
+
extension Variant_GlyphImage_AssetImage_RemoteImage: ImageProtocol {
|
|
25
30
|
var glyphImage: GlyphImage? {
|
|
26
31
|
if case .first(let glyph) = self { return glyph }
|
|
27
32
|
return nil
|
|
@@ -30,4 +35,8 @@ extension Variant_GlyphImage_AssetImage: ImageProtocol {
|
|
|
30
35
|
if case .second(let asset) = self { return asset }
|
|
31
36
|
return nil
|
|
32
37
|
}
|
|
38
|
+
var remoteImage: RemoteImage? {
|
|
39
|
+
if case .third(let remote) = self { return remote }
|
|
40
|
+
return nil
|
|
41
|
+
}
|
|
33
42
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import AVFoundation
|
|
1
2
|
import CarPlay
|
|
2
3
|
import NitroModules
|
|
3
4
|
|
|
@@ -20,6 +21,7 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
20
21
|
private static var listeners = [EventName: [StateListener]]()
|
|
21
22
|
private static var renderStateListeners = [String: [RenderStateListener]]()
|
|
22
23
|
private static var safeAreaInsetsListeners = [String: [SafeAreaListener]]()
|
|
24
|
+
private static var voiceInputManager: VoiceInputManager?
|
|
23
25
|
|
|
24
26
|
override init() {
|
|
25
27
|
HybridAutoPlay.listeners.removeAll()
|
|
@@ -117,10 +119,55 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
117
119
|
func addListenerVoiceInput(
|
|
118
120
|
callback: @escaping (Location?, String?) -> Void
|
|
119
121
|
) throws -> () -> Void {
|
|
120
|
-
//
|
|
122
|
+
// iOS does not use the OS-triggered voice input path — use startVoiceInput() instead.
|
|
121
123
|
return {}
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
func hasVoiceInputPermission() throws -> Bool {
|
|
127
|
+
return AVAudioSession.sharedInstance().recordPermission == .granted
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func requestVoiceInputPermission() throws -> Promise<Bool> {
|
|
131
|
+
return Promise.async {
|
|
132
|
+
return await withCheckedContinuation { cont in
|
|
133
|
+
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
|
134
|
+
cont.resume(returning: granted)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func startVoiceInput(silenceThresholdMs: Double?, maxDurationMs: Double?, listeningText: String?) throws -> Promise<
|
|
141
|
+
ArrayBuffer
|
|
142
|
+
> {
|
|
143
|
+
return Promise.async {
|
|
144
|
+
let interfaceController = try? await RootModule.withInterfaceController { $0 }
|
|
145
|
+
|
|
146
|
+
let manager = VoiceInputManager()
|
|
147
|
+
HybridAutoPlay.voiceInputManager = manager
|
|
148
|
+
|
|
149
|
+
defer {
|
|
150
|
+
HybridAutoPlay.voiceInputManager = nil
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let data = try await manager.start(
|
|
154
|
+
interfaceController: interfaceController,
|
|
155
|
+
silenceThresholdMs: silenceThresholdMs ?? 1_500,
|
|
156
|
+
maxDurationMs: maxDurationMs ?? 10_000,
|
|
157
|
+
listeningText: listeningText ?? "Listening..."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return try ArrayBuffer.copy(data: data)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func stopVoiceInput() throws {
|
|
165
|
+
Task { @MainActor in
|
|
166
|
+
let interfaceController = try? await RootModule.withInterfaceController { $0 }
|
|
167
|
+
HybridAutoPlay.voiceInputManager?.stop(interfaceController: interfaceController)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
124
171
|
// MARK: set/push/pop templates
|
|
125
172
|
func setRootTemplate(templateId: String) throws -> Promise<Void> {
|
|
126
173
|
return Promise.async {
|
|
@@ -151,7 +198,7 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
151
198
|
}
|
|
152
199
|
|
|
153
200
|
func pushTemplate(templateId: String) throws
|
|
154
|
-
->
|
|
201
|
+
-> Promise<Void>
|
|
155
202
|
{
|
|
156
203
|
return Promise.async {
|
|
157
204
|
try await RootModule.withSceneAndInterfaceController {
|
|
@@ -201,7 +248,7 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
201
248
|
}
|
|
202
249
|
}
|
|
203
250
|
|
|
204
|
-
func popTemplate(animate: Bool?) throws ->
|
|
251
|
+
func popTemplate(animate: Bool?) throws -> Promise<Void> {
|
|
205
252
|
return Promise.async {
|
|
206
253
|
try await RootModule.withInterfaceController {
|
|
207
254
|
interfaceController in
|
|
@@ -222,7 +269,7 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
222
269
|
}
|
|
223
270
|
}
|
|
224
271
|
|
|
225
|
-
func popToRootTemplate(animate: Bool?) throws ->
|
|
272
|
+
func popToRootTemplate(animate: Bool?) throws -> Promise<Void> {
|
|
226
273
|
return Promise.async {
|
|
227
274
|
try await RootModule.withInterfaceController {
|
|
228
275
|
interfaceController in
|
|
@@ -69,6 +69,13 @@ class GridTemplate: AutoPlayHeaderProviding {
|
|
|
69
69
|
)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
if let remoteImage = button.image.remoteImage {
|
|
73
|
+
image = Parser.parseRemoteImage(
|
|
74
|
+
remoteImage: remoteImage,
|
|
75
|
+
traitCollection: traitCollection
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
guard let image = image else { return nil }
|
|
73
80
|
guard let title = Parser.parseText(text: button.title) else { return nil }
|
|
74
81
|
|
|
@@ -127,6 +127,20 @@ class MapTemplate: AutoPlayHeaderProviding,
|
|
|
127
127
|
button.onPress?()
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
+
if let remoteImage = button.image.remoteImage,
|
|
131
|
+
let icon = Parser.parseRemoteImage(
|
|
132
|
+
remoteImage: remoteImage,
|
|
133
|
+
traitCollection: traitCollection
|
|
134
|
+
)
|
|
135
|
+
{
|
|
136
|
+
return CPMapButton(image: icon) { _ in
|
|
137
|
+
if button.type == .pan {
|
|
138
|
+
self.onPanButtonPress()
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
button.onPress?()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
130
144
|
|
|
131
145
|
return CPMapButton { _ in
|
|
132
146
|
if button.type == .pan {
|
|
@@ -74,6 +74,12 @@ class Parser {
|
|
|
74
74
|
traitCollection: traitCollection
|
|
75
75
|
)
|
|
76
76
|
}
|
|
77
|
+
if let remoteImage = action.image?.remoteImage {
|
|
78
|
+
image = Parser.parseRemoteImage(
|
|
79
|
+
remoteImage: remoteImage,
|
|
80
|
+
traitCollection: traitCollection
|
|
81
|
+
)
|
|
82
|
+
}
|
|
77
83
|
|
|
78
84
|
var button: CPBarButton
|
|
79
85
|
|
|
@@ -771,9 +777,30 @@ class Parser {
|
|
|
771
777
|
)
|
|
772
778
|
}
|
|
773
779
|
|
|
780
|
+
if let remoteImage = image?.remoteImage {
|
|
781
|
+
return Parser.parseRemoteImage(
|
|
782
|
+
remoteImage: remoteImage,
|
|
783
|
+
traitCollection: traitCollection
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
|
|
774
787
|
return nil
|
|
775
788
|
}
|
|
776
789
|
|
|
790
|
+
// MARK: - Remote image cache
|
|
791
|
+
private static let remoteImageCache: NSCache<NSString, UIImage> = {
|
|
792
|
+
let cache = NSCache<NSString, UIImage>()
|
|
793
|
+
cache.countLimit = 50
|
|
794
|
+
cache.totalCostLimit = 8 * 1024 * 1024 // 8 MB, matching Android's BitmapCache
|
|
795
|
+
return cache
|
|
796
|
+
}()
|
|
797
|
+
|
|
798
|
+
/// Shared session — long-lived by design; failed tasks don't invalidate it.
|
|
799
|
+
private static let remoteImageSession = URLSession(configuration: .default)
|
|
800
|
+
|
|
801
|
+
/// Default network timeout for remote images when no `timeoutMs` is provided.
|
|
802
|
+
private static let defaultRemoteTimeoutSeconds: TimeInterval = 0.5
|
|
803
|
+
|
|
777
804
|
static func parseAssetImage(
|
|
778
805
|
assetImage: AssetImage,
|
|
779
806
|
traitCollection: UITraitCollection
|
|
@@ -784,17 +811,77 @@ class Parser {
|
|
|
784
811
|
"__packager_asset": assetImage.packager_asset,
|
|
785
812
|
])
|
|
786
813
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
814
|
+
return applyTint(
|
|
815
|
+
uiImage: uiImage,
|
|
816
|
+
color: assetImage.color,
|
|
817
|
+
traitCollection: traitCollection
|
|
818
|
+
)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
static func parseRemoteImage(
|
|
822
|
+
remoteImage: RemoteImage,
|
|
823
|
+
traitCollection: UITraitCollection
|
|
824
|
+
) -> UIImage? {
|
|
825
|
+
let timeoutSeconds = remoteImage.timeoutMs.map { $0 / 1000.0 } ?? defaultRemoteTimeoutSeconds
|
|
826
|
+
let uiImage = loadRemoteImage(uri: remoteImage.uri, timeoutSeconds: timeoutSeconds)
|
|
827
|
+
|
|
828
|
+
return applyTint(
|
|
829
|
+
uiImage: uiImage,
|
|
830
|
+
color: remoteImage.color,
|
|
831
|
+
traitCollection: traitCollection
|
|
832
|
+
)
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
private static func applyTint(
|
|
836
|
+
uiImage: UIImage?,
|
|
837
|
+
color: NitroColor?,
|
|
838
|
+
traitCollection: UITraitCollection
|
|
839
|
+
) -> UIImage? {
|
|
840
|
+
guard let image = uiImage else { return nil }
|
|
841
|
+
guard let color else { return image }
|
|
790
842
|
|
|
791
843
|
return getTintedImageAsset(
|
|
792
844
|
color: color,
|
|
793
|
-
uiImage:
|
|
845
|
+
uiImage: image,
|
|
794
846
|
traitCollection: traitCollection
|
|
795
847
|
)
|
|
796
848
|
}
|
|
797
849
|
|
|
850
|
+
/// Synchronously loads an image from a remote HTTPS URL with in-memory caching.
|
|
851
|
+
/// `parseRemoteImage` is always invoked on a background thread by the Car App rendering pipeline,
|
|
852
|
+
/// so the semaphore wait cannot block the main thread.
|
|
853
|
+
private static func loadRemoteImage(uri: String, timeoutSeconds: TimeInterval) -> UIImage? {
|
|
854
|
+
let cacheKey = uri as NSString
|
|
855
|
+
if let cached = remoteImageCache.object(forKey: cacheKey) {
|
|
856
|
+
return cached
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
guard let url = URL(string: uri) else { return nil }
|
|
860
|
+
|
|
861
|
+
var request = URLRequest(url: url)
|
|
862
|
+
request.timeoutInterval = timeoutSeconds
|
|
863
|
+
|
|
864
|
+
var resultData: Data?
|
|
865
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
866
|
+
let task = remoteImageSession.dataTask(with: request) { data, _, _ in
|
|
867
|
+
resultData = data
|
|
868
|
+
semaphore.signal()
|
|
869
|
+
}
|
|
870
|
+
task.resume()
|
|
871
|
+
if semaphore.wait(timeout: .now() + timeoutSeconds) == .timedOut {
|
|
872
|
+
task.cancel()
|
|
873
|
+
return UIImage(systemName: "exclamationmark.circle")
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
guard let data = resultData, let image = UIImage(data: data) else {
|
|
877
|
+
return UIImage(systemName: "exclamationmark.circle")
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
let cost = Int(image.size.width * image.size.height * image.scale * image.scale * 4)
|
|
881
|
+
remoteImageCache.setObject(image, forKey: cacheKey, cost: cost)
|
|
882
|
+
return image
|
|
883
|
+
}
|
|
884
|
+
|
|
798
885
|
static func getTintedImageAsset(
|
|
799
886
|
color: NitroColor,
|
|
800
887
|
uiImage: UIImage,
|