craft-native 0.0.223 → 0.0.225
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/src/index.js +1 -0
- package/dist/android/templates/CraftNativeImageCache.kt.template +141 -0
- package/dist/android/templates/MainActivityNative.kt.template +58 -7
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftHybrid.swift +16 -1
- package/dist/ios/templates/CraftNativeScreen.swift +5 -0
- package/package.json +1 -1
|
@@ -549,6 +549,7 @@ function writeNativeShellSources(output, packagePath, packageName, config) {
|
|
|
549
549
|
};
|
|
550
550
|
write("CraftBluetooth.kt.template", "CraftBluetooth.kt");
|
|
551
551
|
write("CraftNativeShell.kt.template", "CraftNativeShell.kt");
|
|
552
|
+
write("CraftNativeImageCache.kt.template", "CraftNativeImageCache.kt");
|
|
552
553
|
write("CraftNativeBackground.kt.template", "CraftNativeBackground.kt");
|
|
553
554
|
write(config.enableHealthConnect ? "CraftNativeHealth.kt.template" : "CraftNativeHealthStub.kt.template", "CraftNativeHealth.kt");
|
|
554
555
|
write("CraftNativeServices.kt.template", "CraftNativeServices.kt", (source) => source.replace("{{NATIVE_PUSH_IMPORTS}}", config.enablePushNotifications ? "import com.google.firebase.messaging.FirebaseMessaging" : "").replace("{{NATIVE_PUSH_REGISTER}}", config.enablePushNotifications ? ` try {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import java.io.File
|
|
4
|
+
import java.io.BufferedInputStream
|
|
5
|
+
import java.io.DataInputStream
|
|
6
|
+
import java.io.DataOutputStream
|
|
7
|
+
import java.io.IOException
|
|
8
|
+
import java.io.OutputStream
|
|
9
|
+
import java.security.MessageDigest
|
|
10
|
+
import java.util.Locale
|
|
11
|
+
|
|
12
|
+
internal data class CraftNativeImagePolicy(val expiresAt: Long)
|
|
13
|
+
|
|
14
|
+
internal fun craftNativeImagePolicy(
|
|
15
|
+
requestHeaders: Map<String, String>,
|
|
16
|
+
responseHeaders: Map<String, List<String>>,
|
|
17
|
+
now: Long,
|
|
18
|
+
): CraftNativeImagePolicy? {
|
|
19
|
+
// Persist only the anonymous content the server explicitly calls public.
|
|
20
|
+
// A custom header can carry an account credential just like Authorization.
|
|
21
|
+
if (requestHeaders.isNotEmpty()) return null
|
|
22
|
+
fun values(name: String) = responseHeaders.entries
|
|
23
|
+
.filter { it.key.equals(name, ignoreCase = true) }.flatMap { it.value }
|
|
24
|
+
val variants = values("Vary").flatMap { it.split(',') }.map { it.trim().lowercase(Locale.ROOT) }
|
|
25
|
+
if (values("Set-Cookie").isNotEmpty() || variants.any { it in setOf("*", "cookie", "authorization") }) return null
|
|
26
|
+
val directives = values("Cache-Control").flatMap { it.split(',') }.map { it.trim().lowercase(Locale.ROOT) }
|
|
27
|
+
if ("public" !in directives || directives.any { it.substringBefore('=').trim() in setOf("private", "no-store", "no-cache") }) return null
|
|
28
|
+
val ages = directives.filter { it.substringBefore('=').trim() == "max-age" }
|
|
29
|
+
if (ages.isEmpty()) return null
|
|
30
|
+
val seconds = ages.map {
|
|
31
|
+
it.substringAfter('=', "").trim().removeSurrounding("\"").toLongOrNull()?.takeIf { age -> age > 0 } ?: return null
|
|
32
|
+
}.min().coerceAtMost(31_536_000L)
|
|
33
|
+
val ageHeader = values("Age").firstOrNull()
|
|
34
|
+
val age = if (ageHeader == null) 0L else ageHeader.trim().toLongOrNull()?.takeIf { it >= 0 } ?: return null
|
|
35
|
+
val remaining = seconds - age
|
|
36
|
+
if (remaining <= 0) return null
|
|
37
|
+
return runCatching { CraftNativeImagePolicy(Math.addExact(now, remaining * 1000)) }.getOrNull()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
internal class CraftNativeImageCache(
|
|
41
|
+
private val root: File,
|
|
42
|
+
private val maxBytes: Long = 32L * 1024 * 1024,
|
|
43
|
+
private val maxEntryBytes: Long = 8L * 1024 * 1024,
|
|
44
|
+
private val maxEntries: Int = 128,
|
|
45
|
+
private val now: () -> Long = System::currentTimeMillis,
|
|
46
|
+
) {
|
|
47
|
+
init {
|
|
48
|
+
require(maxBytes > 0 && maxEntryBytes > 0 && maxEntries > 0)
|
|
49
|
+
// A process killed while encoding can leave a staging file behind.
|
|
50
|
+
// All instances share this lock, so another writer cannot own it now.
|
|
51
|
+
synchronized(lock) {
|
|
52
|
+
runCatching {
|
|
53
|
+
root.listFiles().orEmpty().filter { it.name.endsWith(".tmp") }.forEach(File::delete)
|
|
54
|
+
prune()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private fun file(address: String): File {
|
|
60
|
+
val digest = MessageDigest.getInstance("SHA-256").digest(address.toByteArray(Charsets.UTF_8))
|
|
61
|
+
val name = digest.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }
|
|
62
|
+
return File(root, "$name.cache")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fun read(address: String): ByteArray? = synchronized(lock) {
|
|
66
|
+
runCatching {
|
|
67
|
+
val target = file(address)
|
|
68
|
+
if (!target.isFile) return@runCatching null
|
|
69
|
+
if (target.length() <= HEADER_BYTES || target.length() > HEADER_BYTES + minOf(maxEntryBytes, maxBytes)) {
|
|
70
|
+
target.delete()
|
|
71
|
+
return@runCatching null
|
|
72
|
+
}
|
|
73
|
+
val data = DataInputStream(BufferedInputStream(target.inputStream())).use { input ->
|
|
74
|
+
if (input.readInt() != MAGIC || input.readLong() <= now()) null else input.readBytes()
|
|
75
|
+
}
|
|
76
|
+
if (data == null || data.isEmpty()) { target.delete(); null }
|
|
77
|
+
else { target.setLastModified(now()); data }
|
|
78
|
+
}.getOrNull()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun write(address: String, policy: CraftNativeImagePolicy, encode: (OutputStream) -> Boolean): Boolean = synchronized(lock) {
|
|
82
|
+
var staging: File? = null
|
|
83
|
+
try {
|
|
84
|
+
if (policy.expiresAt <= now() || (!root.isDirectory && !root.mkdirs())) return@synchronized false
|
|
85
|
+
val target = file(address)
|
|
86
|
+
staging = File.createTempFile(target.name, ".tmp", root)
|
|
87
|
+
staging.outputStream().buffered().use { raw ->
|
|
88
|
+
val header = DataOutputStream(raw)
|
|
89
|
+
header.writeInt(MAGIC)
|
|
90
|
+
header.writeLong(policy.expiresAt)
|
|
91
|
+
val payload = BoundedImageOutput(raw, minOf(maxEntryBytes, maxBytes))
|
|
92
|
+
if (!encode(payload) || payload.written == 0L) throw IOException("Incomplete cached image")
|
|
93
|
+
payload.flush()
|
|
94
|
+
}
|
|
95
|
+
// Freshness and pixels are committed together; a failed write
|
|
96
|
+
// never replaces an earlier, complete image with a partial file.
|
|
97
|
+
if (!staging.renameTo(target)) return@synchronized false
|
|
98
|
+
target.setLastModified(now())
|
|
99
|
+
prune()
|
|
100
|
+
true
|
|
101
|
+
}
|
|
102
|
+
catch (error: Exception) { false }
|
|
103
|
+
finally { staging?.delete() }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fun remove(address: String) = synchronized(lock) { runCatching { file(address).delete() }; Unit }
|
|
107
|
+
|
|
108
|
+
private fun prune() {
|
|
109
|
+
val entries = root.listFiles().orEmpty().filter { entryName.matches(it.name) }.sortedBy(File::lastModified)
|
|
110
|
+
var count = entries.size
|
|
111
|
+
var bytes = entries.sumOf { (it.length() - HEADER_BYTES).coerceAtLeast(0) }
|
|
112
|
+
for (entry in entries) {
|
|
113
|
+
if (count <= maxEntries && bytes <= maxBytes) break
|
|
114
|
+
val size = (entry.length() - HEADER_BYTES).coerceAtLeast(0)
|
|
115
|
+
if (entry.delete()) { count--; bytes -= size }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private class BoundedImageOutput(private val output: OutputStream, private val limit: Long) : OutputStream() {
|
|
120
|
+
var written = 0L
|
|
121
|
+
private set
|
|
122
|
+
override fun write(value: Int) {
|
|
123
|
+
if (written >= limit) throw IOException("Cached image is too large")
|
|
124
|
+
output.write(value)
|
|
125
|
+
written++
|
|
126
|
+
}
|
|
127
|
+
override fun write(bytes: ByteArray, offset: Int, length: Int) {
|
|
128
|
+
if (written + length > limit) throw IOException("Cached image is too large")
|
|
129
|
+
output.write(bytes, offset, length)
|
|
130
|
+
written += length
|
|
131
|
+
}
|
|
132
|
+
override fun flush() = output.flush()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private companion object {
|
|
136
|
+
const val MAGIC = 0x43494d31
|
|
137
|
+
const val HEADER_BYTES = 12L
|
|
138
|
+
val entryName = Regex("^[a-f0-9]{64}\\.cache$")
|
|
139
|
+
val lock = Any()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -92,6 +92,7 @@ import android.widget.Switch
|
|
|
92
92
|
import android.widget.TextView
|
|
93
93
|
import android.widget.MediaController
|
|
94
94
|
import android.widget.VideoView
|
|
95
|
+
import androidx.activity.OnBackPressedCallback
|
|
95
96
|
import androidx.appcompat.app.AppCompatActivity
|
|
96
97
|
import androidx.appcompat.app.AppCompatDelegate
|
|
97
98
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
@@ -121,6 +122,8 @@ import com.google.android.gms.maps.model.Polyline
|
|
|
121
122
|
import com.google.android.gms.maps.model.PolylineOptions
|
|
122
123
|
import org.json.JSONArray
|
|
123
124
|
import org.json.JSONObject
|
|
125
|
+
import java.io.File
|
|
126
|
+
import java.net.HttpURLConnection
|
|
124
127
|
import java.net.URL
|
|
125
128
|
import java.text.SimpleDateFormat
|
|
126
129
|
import java.util.Calendar
|
|
@@ -3804,6 +3807,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3804
3807
|
private var windowSafeInsets = intArrayOf(0, 0, 0, 0)
|
|
3805
3808
|
private var useMessagePorts = false
|
|
3806
3809
|
private val imageExecutor = Executors.newFixedThreadPool(2)
|
|
3810
|
+
private val imageCache by lazy { CraftNativeImageCache(File(cacheDir, "craft-native-images")) }
|
|
3807
3811
|
/** Lays out each isolate's prelude (the synchronous stores) off the main thread. */
|
|
3808
3812
|
private val preludeExecutor = Executors.newSingleThreadExecutor()
|
|
3809
3813
|
private val imageSources = mutableMapOf<ImageView, String>()
|
|
@@ -3826,6 +3830,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
3826
3830
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
3827
3831
|
applyConfiguredAppearance()
|
|
3828
3832
|
super.onCreate(savedInstanceState)
|
|
3833
|
+
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
|
3834
|
+
override fun handleOnBackPressed() = handleNativeBack()
|
|
3835
|
+
})
|
|
3829
3836
|
initialDeepLink = savedInstanceState?.getString("craft.initialDeepLink")?.let(Uri::parse) ?: intent?.data
|
|
3830
3837
|
initialDeepLinkClaimed = savedInstanceState?.getBoolean("craft.initialDeepLinkClaimed", false) ?: false
|
|
3831
3838
|
deliveredInitialDeepLink = savedInstanceState?.getBoolean("craft.deliveredInitialDeepLink", false) ?: false
|
|
@@ -3987,8 +3994,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
3987
3994
|
}
|
|
3988
3995
|
|
|
3989
3996
|
private fun onBackPressedDispatcherCompat() {
|
|
3990
|
-
|
|
3991
|
-
onBackPressed()
|
|
3997
|
+
onBackPressedDispatcher.onBackPressed()
|
|
3992
3998
|
}
|
|
3993
3999
|
|
|
3994
4000
|
private fun haptic() {
|
|
@@ -4153,13 +4159,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
4153
4159
|
* page on the app's site in the in-app browser. Another site's address
|
|
4154
4160
|
* opens in the browser too.
|
|
4155
4161
|
*/
|
|
4156
|
-
private fun navigateOpen(target: String, fromLink: Boolean = false) {
|
|
4162
|
+
private fun navigateOpen(target: String, fromLink: Boolean = false, replace: Boolean = false) {
|
|
4157
4163
|
val path = routes.appPath(target, urlSchemes)
|
|
4158
4164
|
if (path == null) {
|
|
4159
4165
|
openExternal(target)
|
|
4160
4166
|
return
|
|
4161
4167
|
}
|
|
4162
4168
|
val match = routes.match(path)
|
|
4169
|
+
if (replace && match != null && screens.isNotEmpty()) {
|
|
4170
|
+
val stack = screens
|
|
4171
|
+
val removed = stack.removeAt(stack.lastIndex)
|
|
4172
|
+
val previous = stack.lastOrNull()
|
|
4173
|
+
closeScreen(removed) {
|
|
4174
|
+
if (screens === stack && stack.lastOrNull() === previous) navigateOpen(path, fromLink)
|
|
4175
|
+
}
|
|
4176
|
+
return
|
|
4177
|
+
}
|
|
4163
4178
|
val tab = routes.tabFor(path)
|
|
4164
4179
|
if (tab != null) {
|
|
4165
4180
|
selectTab(routes.tabs.indexOf(tab))
|
|
@@ -4767,6 +4782,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
4767
4782
|
// app script has no stack of its own; it uses NAVIGATE_OPEN.
|
|
4768
4783
|
if (visibleScreen !== screen) return
|
|
4769
4784
|
val route = payload.optString("screen").takeIf { it.isNotBlank() } ?: return
|
|
4785
|
+
// Resolve and replace in one host operation. A pop followed by
|
|
4786
|
+
// open would send the second message from a closing isolate.
|
|
4787
|
+
if (message.optString("type") == "NAVIGATE_REPLACE" && route.startsWith("/")) {
|
|
4788
|
+
navigateOpen(route, replace = true)
|
|
4789
|
+
return
|
|
4790
|
+
}
|
|
4770
4791
|
if (message.optString("type") == "NAVIGATE_REPLACE") {
|
|
4771
4792
|
val stack = screens
|
|
4772
4793
|
val removed = stack.removeAt(stack.lastIndex)
|
|
@@ -6098,7 +6119,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
6098
6119
|
null
|
|
6099
6120
|
} else defaultBackground
|
|
6100
6121
|
val overflow = craftNativeOptionalString(style.opt("overflow"))
|
|
6101
|
-
|
|
6122
|
+
// A replaced image clips its own pixels to border-radius, including
|
|
6123
|
+
// when its surrounding layout permits visible overflow.
|
|
6124
|
+
view.clipToOutline = overflow == "hidden" || (view is ImageView && (radius > 0 || hasCornerRadii))
|
|
6102
6125
|
if (view is ViewGroup) view.clipChildren = overflow == "hidden"
|
|
6103
6126
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
|
|
6104
6127
|
(style.has("shadowColor") || style.has("shadowOpacity") || style.has("shadowRadius"))
|
|
@@ -6496,12 +6519,38 @@ class MainActivity : AppCompatActivity() {
|
|
|
6496
6519
|
}
|
|
6497
6520
|
imageJobs[view] = imageExecutor.submit {
|
|
6498
6521
|
val bitmap = runCatching {
|
|
6499
|
-
val
|
|
6522
|
+
val publicRequest = headers.isEmpty() && Uri.parse(uri).userInfo.isNullOrEmpty()
|
|
6523
|
+
if (publicRequest) {
|
|
6524
|
+
val bytes = imageCache.read(uri)
|
|
6525
|
+
if (bytes != null) {
|
|
6526
|
+
val saved = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
6527
|
+
if (saved != null) return@runCatching saved
|
|
6528
|
+
imageCache.remove(uri)
|
|
6529
|
+
}
|
|
6530
|
+
}
|
|
6531
|
+
val connection = (URL(uri).openConnection() as HttpURLConnection).apply {
|
|
6500
6532
|
connectTimeout = 10_000
|
|
6501
6533
|
readTimeout = 10_000
|
|
6534
|
+
// This loader owns its bounded public cache. Do not let
|
|
6535
|
+
// an application's global ResponseCache retain requests.
|
|
6536
|
+
useCaches = false
|
|
6502
6537
|
headers.forEach { (name, value) -> setRequestProperty(name, value) }
|
|
6503
6538
|
}
|
|
6504
|
-
|
|
6539
|
+
try {
|
|
6540
|
+
val downloaded = connection.inputStream.use(BitmapFactory::decodeStream)
|
|
6541
|
+
if (publicRequest) {
|
|
6542
|
+
val responseHeaders = connection.headerFields.entries.filter { it.key != null }
|
|
6543
|
+
.associate { it.key!! to it.value.orEmpty() }
|
|
6544
|
+
val policy = if (connection.responseCode == HttpURLConnection.HTTP_OK)
|
|
6545
|
+
craftNativeImagePolicy(headers, responseHeaders, System.currentTimeMillis()) else null
|
|
6546
|
+
if (downloaded != null && policy != null) {
|
|
6547
|
+
// Encode only after a complete decode, then commit
|
|
6548
|
+
// before onLoad so a cold offline reopen is safe.
|
|
6549
|
+
imageCache.write(uri, policy) { downloaded.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
|
6550
|
+
} else imageCache.remove(uri)
|
|
6551
|
+
}
|
|
6552
|
+
downloaded
|
|
6553
|
+
} finally { connection.disconnect() }
|
|
6505
6554
|
}.getOrNull()
|
|
6506
6555
|
runOnUiThread {
|
|
6507
6556
|
if (imageSources[view] != requestKey) return@runOnUiThread
|
|
@@ -7286,7 +7335,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
7286
7335
|
}
|
|
7287
7336
|
|
|
7288
7337
|
@Deprecated("Uses the activity back callback for native route navigation")
|
|
7289
|
-
override fun onBackPressed()
|
|
7338
|
+
override fun onBackPressed() = handleNativeBack()
|
|
7339
|
+
|
|
7340
|
+
private fun handleNativeBack() {
|
|
7290
7341
|
val screen = screens.lastOrNull() ?: return back()
|
|
7291
7342
|
val modal = screen.modalVisibility.entries.lastOrNull { (view, visible) -> visible && view.visibility == View.VISIBLE }
|
|
7292
7343
|
if (modal != null) {
|
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.225";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -266,6 +266,8 @@ protocol CraftHybridScreenEvents: AnyObject {
|
|
|
266
266
|
func nativeScreen(_ screen: CraftNativeScreenController, didFail message: String)
|
|
267
267
|
/// `craft.navigation.open(path)`.
|
|
268
268
|
func nativeScreen(_ screen: CraftNativeScreenController, open path: String)
|
|
269
|
+
/// `craft.navigation.replace(path)`: finish the current flow atomically.
|
|
270
|
+
func nativeScreen(_ screen: CraftNativeScreenController, replace path: String)
|
|
269
271
|
}
|
|
270
272
|
|
|
271
273
|
// MARK: - The shell
|
|
@@ -938,6 +940,10 @@ extension CraftHybrid: CraftHybridScreenEvents {
|
|
|
938
940
|
func nativeScreen(_ screen: CraftNativeScreenController, open path: String) {
|
|
939
941
|
open(path)
|
|
940
942
|
}
|
|
943
|
+
|
|
944
|
+
func nativeScreen(_ screen: CraftNativeScreenController, replace path: String) {
|
|
945
|
+
controller?.showNative(path: path, replace: true)
|
|
946
|
+
}
|
|
941
947
|
}
|
|
942
948
|
|
|
943
949
|
/// The page's `craftHybrid` channel, only from a trusted origin's main frame.
|
|
@@ -1519,6 +1525,10 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1519
1525
|
interruptTabSwipe(restoringSelection: true)
|
|
1520
1526
|
let tabs = hybrid.tabIds
|
|
1521
1527
|
if let tab = tabs.first(where: { CraftHybridRoutes.normalize($0) == match.path }) {
|
|
1528
|
+
// Finish the flow in its original tab before selecting another.
|
|
1529
|
+
if replace, let current = visibleStack, current.viewControllers.count > 1 {
|
|
1530
|
+
current.setViewControllers(Array(current.viewControllers.dropLast()), animated: false)
|
|
1531
|
+
}
|
|
1522
1532
|
if tab != currentTab {
|
|
1523
1533
|
selectTabAsIfTapped(tab)
|
|
1524
1534
|
}
|
|
@@ -1541,7 +1551,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1541
1551
|
return
|
|
1542
1552
|
}
|
|
1543
1553
|
let screen = entry(for: raw, isTabRoot: false, depth: nil)
|
|
1544
|
-
if replace, nav.viewControllers.
|
|
1554
|
+
if replace, !nav.viewControllers.isEmpty {
|
|
1545
1555
|
nav.setViewControllers(Array(nav.viewControllers.dropLast()) + [screen], animated: true)
|
|
1546
1556
|
} else {
|
|
1547
1557
|
nav.pushViewController(screen, animated: true)
|
|
@@ -1900,6 +1910,11 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1900
1910
|
open(path)
|
|
1901
1911
|
}
|
|
1902
1912
|
|
|
1913
|
+
func nativeScreen(_ screen: CraftNativeScreenController, replace path: String) {
|
|
1914
|
+
guard screen.navigationController?.topViewController === screen else { return }
|
|
1915
|
+
showNative(path: path, replace: true)
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1903
1918
|
private func log(_ message: String) {
|
|
1904
1919
|
#if DEBUG
|
|
1905
1920
|
NSLog("[craft hybrid] %@", message)
|
|
@@ -5416,6 +5416,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5416
5416
|
guard !isAppScript, navigationController?.topViewController === self,
|
|
5417
5417
|
let screen = payload["screen"] as? String, !screen.isEmpty,
|
|
5418
5418
|
let navigation = navigationController else { return }
|
|
5419
|
+
if type == "NAVIGATE_REPLACE", screen.hasPrefix("/") {
|
|
5420
|
+
hybridEvents?.nativeScreen(self, replace: screen)
|
|
5421
|
+
return
|
|
5422
|
+
}
|
|
5419
5423
|
let params = payload["params"] as? [String: Any] ?? [:]
|
|
5420
5424
|
let next = CraftNativeScreenController(config: config, routeName: screen, routeParams: params)
|
|
5421
5425
|
next.hybridEvents = hybridEvents
|
|
@@ -7006,6 +7010,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7006
7010
|
view.clipsToBounds = overflow == "hidden"
|
|
7007
7011
|
|| (view is UIScrollView && overflow != "visible")
|
|
7008
7012
|
|| (view is UILabel && (view.craftRequestedCornerRadius ?? 0) > 0)
|
|
7013
|
+
|| (view is UIImageView && (borderRadius > 0 || hasCornerRadii))
|
|
7009
7014
|
view.layer.masksToBounds = view.clipsToBounds
|
|
7010
7015
|
view.craftLayoutStyle = CraftNativeLayoutStyle(style)
|
|
7011
7016
|
(view as? CraftNativeFlowView)?.invalidateMeasurements()
|