craft-native 0.0.187 → 0.0.191
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/dist/android/templates/CraftNative.kt.template +34 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +92 -9
- package/dist/android/templates/CraftNativeShell.kt.template +58 -5
- package/dist/android/templates/MainActivityNative.kt.template +354 -34
- package/dist/browser/dialogs.d.ts +20 -0
- package/dist/browser/storage.d.ts +31 -0
- package/dist/browser.d.ts +22 -0
- package/dist/browser.js +705 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftApp.swift +43 -4
- package/dist/ios/templates/CraftHybrid.swift +36 -4
- package/dist/ios/templates/CraftNativeActions.swift +16 -4
- package/dist/ios/templates/CraftNativeFiles.swift +131 -25
- package/dist/ios/templates/CraftNativeScreen.swift +457 -3
- package/dist/macos/templates/native_layout.zig +299 -6
- package/package.json +9 -1
|
@@ -341,6 +341,18 @@ object CraftNative {
|
|
|
341
341
|
nodeData: FloatArray,
|
|
342
342
|
treeMeta: FloatArray,
|
|
343
343
|
): FloatArray?
|
|
344
|
+
private external fun nativeLayoutTreeV2(
|
|
345
|
+
width: Float,
|
|
346
|
+
height: Float,
|
|
347
|
+
nodeData: FloatArray,
|
|
348
|
+
treeMeta: FloatArray,
|
|
349
|
+
): FloatArray?
|
|
350
|
+
private external fun nativeLayoutTreeV3(
|
|
351
|
+
width: Float,
|
|
352
|
+
height: Float,
|
|
353
|
+
nodeData: FloatArray,
|
|
354
|
+
treeMeta: FloatArray,
|
|
355
|
+
): FloatArray?
|
|
344
356
|
private external fun nativeClipboardRead(activity: Activity): String?
|
|
345
357
|
private external fun nativeClipboardWrite(activity: Activity, text: String): Boolean
|
|
346
358
|
private external fun nativeOpenUrl(activity: Activity, url: String): Boolean
|
|
@@ -601,6 +613,28 @@ object CraftNative {
|
|
|
601
613
|
}
|
|
602
614
|
}
|
|
603
615
|
|
|
616
|
+
/** Resolve a reverse-aware tree in Zig, or null for Kotlin fallback. */
|
|
617
|
+
fun layoutTreeV2(width: Float, height: Float, nodeData: FloatArray, treeMeta: FloatArray): FloatArray? {
|
|
618
|
+
if (!isAvailable || nodeData.isEmpty() || treeMeta.isEmpty()) return null
|
|
619
|
+
return try {
|
|
620
|
+
nativeLayoutTreeV2(width, height, nodeData, treeMeta)
|
|
621
|
+
?.takeIf { it.size >= treeMeta.size / 2 * 4 && it.all { value -> value.isFinite() } }
|
|
622
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
623
|
+
null
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Resolve a wrap-reverse-aware tree in Zig, or null for Kotlin fallback. */
|
|
628
|
+
fun layoutTreeV3(width: Float, height: Float, nodeData: FloatArray, treeMeta: FloatArray): FloatArray? {
|
|
629
|
+
if (!isAvailable || nodeData.isEmpty() || treeMeta.isEmpty()) return null
|
|
630
|
+
return try {
|
|
631
|
+
nativeLayoutTreeV3(width, height, nodeData, treeMeta)
|
|
632
|
+
?.takeIf { it.size >= treeMeta.size / 2 * 4 && it.all { value -> value.isFinite() } }
|
|
633
|
+
} catch (e: UnsatisfiedLinkError) {
|
|
634
|
+
null
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
604
638
|
/**
|
|
605
639
|
* The clipboard's text, or null to use the Kotlin implementation.
|
|
606
640
|
*
|
|
@@ -104,6 +104,7 @@ internal class CraftNativeCapabilities(
|
|
|
104
104
|
val maxPixels: Int,
|
|
105
105
|
val quality: Float,
|
|
106
106
|
val completion: (CraftNativeCapabilityResult) -> Unit,
|
|
107
|
+
val videoLimit: Long = 0,
|
|
107
108
|
)
|
|
108
109
|
|
|
109
110
|
init {
|
|
@@ -166,6 +167,7 @@ internal class CraftNativeCapabilities(
|
|
|
166
167
|
}
|
|
167
168
|
"Files" -> when (method) {
|
|
168
169
|
"pickImage" -> pickImage(requestToken, args, completion)
|
|
170
|
+
"pickVideo" -> pickVideo(requestToken, args, completion)
|
|
169
171
|
"download", "write", "remove" -> networkExecutor.execute {
|
|
170
172
|
complete(requestToken, files(requestToken, method, args), completion)
|
|
171
173
|
}
|
|
@@ -238,21 +240,28 @@ internal class CraftNativeCapabilities(
|
|
|
238
240
|
}
|
|
239
241
|
}
|
|
240
242
|
if (body != null || multipart is FileResult.Multipart) doOutput = true
|
|
241
|
-
if (multipart is FileResult.Multipart)
|
|
243
|
+
if (multipart is FileResult.Multipart) {
|
|
244
|
+
setRequestProperty("Content-Type", "multipart/form-data; boundary=${multipart.boundary}")
|
|
245
|
+
setFixedLengthStreamingMode(multipart.body.length())
|
|
246
|
+
readTimeout = 600_000
|
|
247
|
+
}
|
|
242
248
|
}
|
|
243
249
|
} catch (error: IllegalArgumentException) {
|
|
250
|
+
if (multipart is FileResult.Multipart) multipart.body.parentFile?.deleteRecursively()
|
|
244
251
|
return failure("INVALID_ARGUMENT", error.message ?: "fetch request is invalid")
|
|
245
252
|
} catch (error: Exception) {
|
|
253
|
+
if (multipart is FileResult.Multipart) multipart.body.parentFile?.deleteRecursively()
|
|
246
254
|
return failure("NETWORK_ERROR", error.message ?: "Network request failed")
|
|
247
255
|
}
|
|
248
256
|
fetchConnections[requestToken] = connection
|
|
249
257
|
if (cancelled.contains(requestToken)) {
|
|
250
258
|
fetchConnections.remove(requestToken)?.disconnect()
|
|
259
|
+
if (multipart is FileResult.Multipart) multipart.body.parentFile?.deleteRecursively()
|
|
251
260
|
return failure("CANCELLED", "Network request was cancelled")
|
|
252
261
|
}
|
|
253
262
|
return try {
|
|
254
263
|
if (body != null) connection.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) }
|
|
255
|
-
if (multipart is FileResult.Multipart) connection.outputStream.use {
|
|
264
|
+
if (multipart is FileResult.Multipart) connection.outputStream.use { output -> multipart.body.inputStream().use { it.copyTo(output, 64 * 1024) } }
|
|
256
265
|
val status = connection.responseCode
|
|
257
266
|
val input = if (status >= 400) connection.errorStream else connection.inputStream
|
|
258
267
|
val output = ByteArrayOutputStream()
|
|
@@ -283,17 +292,21 @@ internal class CraftNativeCapabilities(
|
|
|
283
292
|
failure("NETWORK_ERROR", error.message ?: "Network request failed")
|
|
284
293
|
} finally {
|
|
285
294
|
fetchConnections.remove(requestToken)?.disconnect()
|
|
295
|
+
if (multipart is FileResult.Multipart) multipart.body.parentFile?.deleteRecursively()
|
|
286
296
|
}
|
|
287
297
|
}
|
|
288
298
|
|
|
289
299
|
private sealed interface FileResult {
|
|
290
|
-
data class Multipart(val body:
|
|
300
|
+
data class Multipart(val body: File, val boundary: String) : FileResult
|
|
291
301
|
data class Failure(val code: String, val message: String) : FileResult
|
|
292
302
|
}
|
|
293
303
|
|
|
294
304
|
private fun multipart(parts: JSONArray): FileResult {
|
|
295
305
|
val boundary = "craft-${UUID.randomUUID()}"
|
|
296
|
-
val
|
|
306
|
+
val file = File(fileRoot, UUID.randomUUID().toString()).apply { mkdirs() }.resolve("upload.multipart")
|
|
307
|
+
var kept = false
|
|
308
|
+
try {
|
|
309
|
+
FileOutputStream(file).use { body ->
|
|
297
310
|
fun append(value: String) { body.write(value.toByteArray(Charsets.UTF_8)) }
|
|
298
311
|
for (index in 0 until parts.length()) {
|
|
299
312
|
val part = parts.optJSONObject(index) ?: return FileResult.Failure("INVALID_ARGUMENT", "A form part must be an object")
|
|
@@ -303,14 +316,11 @@ internal class CraftNativeCapabilities(
|
|
|
303
316
|
val path = part.optString("path")
|
|
304
317
|
if (path.isNotBlank()) {
|
|
305
318
|
val file = ownedFile(path) ?: return FileResult.Failure("INVALID_ARGUMENT", "A form file must be one craft.files made, and still there")
|
|
306
|
-
val bytes = runCatching { file.readBytes() }.getOrElse {
|
|
307
|
-
return FileResult.Failure("INVALID_ARGUMENT", "A form file could not be read")
|
|
308
|
-
}
|
|
309
319
|
val filename = safeFileName(part.optString("filename").ifBlank { file.name })
|
|
310
320
|
val type = part.optString("type").ifBlank { URLConnection.guessContentTypeFromName(filename) ?: "application/octet-stream" }
|
|
311
321
|
append("Content-Disposition: form-data; name=\"${quoted(name)}\"; filename=\"${quoted(filename)}\"\r\n")
|
|
312
322
|
append("Content-Type: ${headerValue(type)}\r\n\r\n")
|
|
313
|
-
|
|
323
|
+
file.inputStream().use { it.copyTo(body, 64 * 1024) }
|
|
314
324
|
append("\r\n")
|
|
315
325
|
} else {
|
|
316
326
|
append("Content-Disposition: form-data; name=\"${quoted(name)}\"\r\n\r\n")
|
|
@@ -319,7 +329,14 @@ internal class CraftNativeCapabilities(
|
|
|
319
329
|
}
|
|
320
330
|
}
|
|
321
331
|
append("--$boundary--\r\n")
|
|
322
|
-
|
|
332
|
+
}
|
|
333
|
+
kept = true
|
|
334
|
+
return FileResult.Multipart(file, boundary)
|
|
335
|
+
} catch (error: Exception) {
|
|
336
|
+
return FileResult.Failure("WRITE_ERROR", error.message ?: "A form file could not be read")
|
|
337
|
+
} finally {
|
|
338
|
+
if (!kept) file.parentFile?.deleteRecursively()
|
|
339
|
+
}
|
|
323
340
|
}
|
|
324
341
|
|
|
325
342
|
private fun files(requestToken: String, method: String, args: JSONArray): CraftNativeCapabilityResult {
|
|
@@ -417,6 +434,64 @@ internal class CraftNativeCapabilities(
|
|
|
417
434
|
return success(JSONObject().put("ok", false).put("status", status).put("body", body.toString(Charsets.UTF_8.name())))
|
|
418
435
|
}
|
|
419
436
|
|
|
437
|
+
private fun pickVideo(requestToken: String, args: JSONArray, completion: (CraftNativeCapabilityResult) -> Unit) {
|
|
438
|
+
mainHandler.post {
|
|
439
|
+
val host = activity ?: return@post complete(requestToken, failure("NEEDS_ACTIVITY", "No screen is showing to pick from"), completion)
|
|
440
|
+
if (pendingFilePick != null) {
|
|
441
|
+
complete(requestToken, failure("BUSY", "Another file picker is already open"), completion)
|
|
442
|
+
return@post
|
|
443
|
+
}
|
|
444
|
+
val options = args.optJSONObject(0) ?: JSONObject()
|
|
445
|
+
val limit = options.optLong("maxBytes", 512L * 1024 * 1024).coerceIn(1, 512L * 1024 * 1024)
|
|
446
|
+
pendingFilePick = PendingFilePick(requestToken, "video", 0, 1f, completion, limit)
|
|
447
|
+
val photos = options.optString("source", "library") != "files" && Build.VERSION.SDK_INT >= 33
|
|
448
|
+
val intent = Intent(if (photos) MediaStore.ACTION_PICK_IMAGES else Intent.ACTION_OPEN_DOCUMENT).apply {
|
|
449
|
+
type = "video/*"
|
|
450
|
+
if (!photos) addCategory(Intent.CATEGORY_OPENABLE)
|
|
451
|
+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
452
|
+
}
|
|
453
|
+
try { host.startActivityForResult(intent, CRAFT_NATIVE_FILE_PICK_REQUEST) }
|
|
454
|
+
catch (error: Exception) {
|
|
455
|
+
pendingFilePick = null
|
|
456
|
+
complete(requestToken, failure("PICKER_UNAVAILABLE", error.message ?: "Video picker could not be opened"), completion)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Copy with a bound even when a cloud provider does not report its size. */
|
|
462
|
+
private fun copyVideo(uri: Uri, limit: Long): CraftNativeCapabilityResult {
|
|
463
|
+
var target: File? = null
|
|
464
|
+
return try {
|
|
465
|
+
val name = context.contentResolver.query(uri, arrayOf(android.provider.OpenableColumns.DISPLAY_NAME), null, null, null)?.use {
|
|
466
|
+
if (it.moveToFirst()) it.getString(0) else null
|
|
467
|
+
} ?: "video.mp4"
|
|
468
|
+
val types = mapOf("mp4" to "video/mp4", "m4v" to "video/x-m4v", "mov" to "video/quicktime", "webm" to "video/webm")
|
|
469
|
+
val type = types[name.substringAfterLast('.', "").lowercase()]
|
|
470
|
+
?: return failure("INVALID_VIDEO", "Choose an MP4, MOV or WebM video.")
|
|
471
|
+
val destination = File(fileRoot, UUID.randomUUID().toString()).apply { mkdirs() }.resolve(safeFileName(name))
|
|
472
|
+
target = destination
|
|
473
|
+
val input = context.contentResolver.openInputStream(uri) ?: throw IllegalArgumentException("The video could not be read")
|
|
474
|
+
var size = 0L
|
|
475
|
+
input.use { source ->
|
|
476
|
+
destination.outputStream().use { output ->
|
|
477
|
+
val buffer = ByteArray(64 * 1024)
|
|
478
|
+
while (true) {
|
|
479
|
+
val count = source.read(buffer)
|
|
480
|
+
if (count < 0) break
|
|
481
|
+
size += count
|
|
482
|
+
require(size <= limit) { "Choose a video under ${limit / 1024 / 1024}MB." }
|
|
483
|
+
output.write(buffer, 0, count)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
require(size > 0) { "Choose a nonempty video." }
|
|
488
|
+
success(descriptor(destination, type))
|
|
489
|
+
} catch (error: Exception) {
|
|
490
|
+
target?.parentFile?.deleteRecursively()
|
|
491
|
+
failure("READ_ERROR", error.message ?: "The video could not be read")
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
420
495
|
private fun pickImage(
|
|
421
496
|
requestToken: String,
|
|
422
497
|
args: JSONArray,
|
|
@@ -465,6 +540,14 @@ internal class CraftNativeCapabilities(
|
|
|
465
540
|
complete(pending.requestToken, success(JSONObject.NULL), pending.completion)
|
|
466
541
|
return true
|
|
467
542
|
}
|
|
543
|
+
if (pending.source == "video") {
|
|
544
|
+
networkExecutor.execute {
|
|
545
|
+
val result = data?.data?.let { copyVideo(it, pending.videoLimit) }
|
|
546
|
+
?: failure("PICKER_FAILED", "The selected video could not be read")
|
|
547
|
+
complete(pending.requestToken, result, pending.completion)
|
|
548
|
+
}
|
|
549
|
+
return true
|
|
550
|
+
}
|
|
468
551
|
val descriptor = if (pending.source == "camera") {
|
|
469
552
|
val bitmap = data?.extras?.get("data") as? Bitmap
|
|
470
553
|
bitmap?.let { writeImage(it, pending.maxPixels, pending.quality, "photo.jpg") }
|
|
@@ -576,10 +576,14 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
576
576
|
private val buttons = LinearLayout(context)
|
|
577
577
|
private val tabBar = LinearLayout(context)
|
|
578
578
|
private val tabViews = mutableListOf<Pair<TextView, TextView>>()
|
|
579
|
+
private val tabIconText = mutableMapOf<String, String>()
|
|
580
|
+
private val tabImageURLs = mutableMapOf<String, String>()
|
|
581
|
+
private val tabImages = mutableMapOf<String, android.graphics.drawable.Drawable>()
|
|
579
582
|
private val density = resources.displayMetrics.density
|
|
580
583
|
private var selectedTab = 0
|
|
581
584
|
private var swipeTracking = false
|
|
582
585
|
private var swipeIntercepted = false
|
|
586
|
+
private var horizontalScrollDeferred = false
|
|
583
587
|
private var swipeDownX = 0f
|
|
584
588
|
private var swipeDownY = 0f
|
|
585
589
|
private val swipeSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
@@ -590,6 +594,8 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
590
594
|
var onTabSwipeStart: ((Float, Float, Float) -> Boolean)? = null
|
|
591
595
|
var onTabSwipeMove: ((Float) -> Unit)? = null
|
|
592
596
|
var onTabSwipeEnd: ((Float, Boolean) -> Unit)? = null
|
|
597
|
+
/** Lets a native horizontal scroller own the gesture from its first down. */
|
|
598
|
+
var onTabSwipeShouldDefer: ((Float, Float) -> Boolean)? = null
|
|
593
599
|
var tabSwipeEnabled = true
|
|
594
600
|
|
|
595
601
|
init {
|
|
@@ -654,7 +660,10 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
654
660
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
655
661
|
when (event.actionMasked) {
|
|
656
662
|
MotionEvent.ACTION_DOWN -> {
|
|
657
|
-
|
|
663
|
+
val overHorizontalScroll = onTabSwipeShouldDefer?.invoke(event.x, event.y) == true
|
|
664
|
+
horizontalScrollDeferred = overHorizontalScroll
|
|
665
|
+
if (overHorizontalScroll) parent?.requestDisallowInterceptTouchEvent(true)
|
|
666
|
+
swipeTracking = tabs.size > 1 && tabSwipeEnabled && !overHorizontalScroll && tabBar.visibility == View.VISIBLE && event.y < tabBar.top
|
|
658
667
|
swipeIntercepted = false
|
|
659
668
|
swipeDownX = event.x
|
|
660
669
|
swipeDownY = event.y
|
|
@@ -668,7 +677,13 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
668
677
|
return swipeIntercepted
|
|
669
678
|
}
|
|
670
679
|
}
|
|
671
|
-
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP ->
|
|
680
|
+
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
|
|
681
|
+
swipeTracking = false
|
|
682
|
+
if (horizontalScrollDeferred) {
|
|
683
|
+
horizontalScrollDeferred = false
|
|
684
|
+
parent?.requestDisallowInterceptTouchEvent(false)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
672
687
|
}
|
|
673
688
|
return swipeIntercepted
|
|
674
689
|
}
|
|
@@ -691,6 +706,7 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
691
706
|
|
|
692
707
|
/** Draws `options` (a screen's `setOptions`) for a screen `depth` deep in its stack. */
|
|
693
708
|
fun apply(options: JSONObject, depth: Int, fallbackTitle: String?) {
|
|
709
|
+
options.optJSONObject("tabItem")?.let(::updateTab)
|
|
694
710
|
val hidden = options.optBoolean("hidden", false)
|
|
695
711
|
val large = options.optBoolean("largeTitle", false) && depth == 0
|
|
696
712
|
val title = options.optString("title").takeIf { options.has("title") } ?: fallbackTitle.orEmpty()
|
|
@@ -716,6 +732,42 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
716
732
|
refreshColors()
|
|
717
733
|
}
|
|
718
734
|
|
|
735
|
+
private fun updateTab(item: JSONObject) {
|
|
736
|
+
val id = item.optString("id")
|
|
737
|
+
val index = tabs.indexOfFirst { it.id == id }
|
|
738
|
+
if (index < 0) return
|
|
739
|
+
val cell = tabBar.getChildAt(index)
|
|
740
|
+
if (item.has("accessibilityLabel")) cell.contentDescription = item.optString("accessibilityLabel").takeUnless { item.isNull("accessibilityLabel") || it.isBlank() } ?: tabs[index].title
|
|
741
|
+
if (item.has("iconText")) {
|
|
742
|
+
tabIconText.remove(id)
|
|
743
|
+
item.optString("iconText").takeUnless { item.isNull("iconText") || it.isBlank() }?.let { tabIconText[id] = it.take(8) }
|
|
744
|
+
}
|
|
745
|
+
if (item.has("image")) {
|
|
746
|
+
val address = item.optString("image").takeIf { !item.isNull("image") && it.startsWith("https://") }
|
|
747
|
+
if (address != tabImageURLs[id]) {
|
|
748
|
+
tabImages.remove(id)
|
|
749
|
+
tabImageURLs.remove(id)
|
|
750
|
+
if (address != null) {
|
|
751
|
+
tabImageURLs[id] = address
|
|
752
|
+
Thread {
|
|
753
|
+
val image = runCatching {
|
|
754
|
+
val connection = java.net.URL(address).openConnection().apply { connectTimeout = 5000; readTimeout = 5000 }
|
|
755
|
+
val bitmap = connection.getInputStream().use { android.graphics.BitmapFactory.decodeStream(it) } ?: return@runCatching null
|
|
756
|
+
androidx.core.graphics.drawable.RoundedBitmapDrawableFactory.create(resources, android.graphics.Bitmap.createScaledBitmap(bitmap, dp(27), dp(27), true)).apply { isCircular = true }
|
|
757
|
+
}.getOrNull()
|
|
758
|
+
post {
|
|
759
|
+
if (tabImageURLs[id] == address && image != null) {
|
|
760
|
+
tabImages[id] = image
|
|
761
|
+
refreshColors()
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}.start()
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
refreshColors()
|
|
769
|
+
}
|
|
770
|
+
|
|
719
771
|
/** Light or dark changed: the bars follow the screen's background. */
|
|
720
772
|
fun refreshColors() {
|
|
721
773
|
val background = ContextCompat.getColor(context, R.color.background)
|
|
@@ -737,11 +789,12 @@ internal class CraftNativeChrome(context: Context, private val tabs: List<CraftN
|
|
|
737
789
|
)).apply { setLayerInset(1, 0, maxOf(1, (density / 2).toInt()), 0, 0) }
|
|
738
790
|
tabViews.forEachIndexed { index, (icon, label) ->
|
|
739
791
|
val color = if (index == selectedTab) accent else muted
|
|
740
|
-
|
|
792
|
+
val number = tabIconText[tabs[index].id]
|
|
793
|
+
icon.setTextColor(if (number != null) Color.parseColor("#FF2D55") else color)
|
|
741
794
|
label.setTextColor(color)
|
|
742
|
-
val vector = CraftNativeIcons.drawable(context, null, tabs[index].symbol, color)
|
|
795
|
+
val vector = tabImages[tabs[index].id] ?: if (number == null) CraftNativeIcons.drawable(context, null, tabs[index].symbol, color) else null
|
|
743
796
|
icon.foreground = vector
|
|
744
|
-
if (vector != null)
|
|
797
|
+
icon.text = if (vector != null) "" else number ?: CraftNativeSymbols.glyph(tabs[index].symbol)
|
|
745
798
|
}
|
|
746
799
|
for (index in 0 until buttons.childCount) (buttons.getChildAt(index) as? TextView)?.setTextColor(accent)
|
|
747
800
|
}
|