craft-native 0.0.169 → 0.0.171
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/CraftNativeServices.kt.template +1 -1
- package/dist/android/templates/CraftNativeShell.kt.template +16 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/templates/CraftNativeActions.swift +15 -0
- package/dist/ios/templates/CraftNativeMutation.swift +19 -1
- package/dist/ios/templates/CraftNativeScreen.swift +16 -1
- package/package.json +1 -1
|
@@ -109,7 +109,7 @@ internal class CraftNativeServices(
|
|
|
109
109
|
"Speech" -> speech(method, args, done)
|
|
110
110
|
"Location" -> location(method, args, done)
|
|
111
111
|
"Media" -> when (method) {
|
|
112
|
-
"keep" -> CraftNativeMedia.keep(context, args.optString(0), args.optJSONObject(1)?.optJSONObject("headers") ?: JSONObject(), done)
|
|
112
|
+
"keep" -> CraftNativeMedia.keep(context, args.optString(0), args.optJSONObject(1)?.optJSONObject("headers") ?: JSONObject(), args.optJSONObject(1)?.optBoolean("cellular", true) ?: true, done)
|
|
113
113
|
"path" -> done(success(CraftNativeMedia.local(context, args.optString(0))?.absolutePath ?: JSONObject.NULL))
|
|
114
114
|
"prune" -> {
|
|
115
115
|
val keep = args.optJSONArray(0) ?: JSONArray()
|
|
@@ -808,12 +808,21 @@ internal object CraftNativeMedia {
|
|
|
808
808
|
|
|
809
809
|
private fun describe(address: String, file: File) = JSONObject().put("ok", true).put("url", address).put("path", file.absolutePath).put("size", file.length())
|
|
810
810
|
|
|
811
|
-
|
|
811
|
+
/**
|
|
812
|
+
* `cellular = false` keeps it off a metered connection (mobile data, a
|
|
813
|
+
* hotspot): nothing is fetched and the answer is `{ ok: false, reason:
|
|
814
|
+
* "cellular" }`, so the screen can say it is waiting for Wi-Fi. A copy
|
|
815
|
+
* already kept still answers, whatever the connection.
|
|
816
|
+
*/
|
|
817
|
+
fun keep(context: Context, address: String, headers: JSONObject, cellular: Boolean, done: (CraftNativeCapabilityResult) -> Unit) {
|
|
812
818
|
val uri = runCatching { Uri.parse(address) }.getOrNull()
|
|
813
819
|
if (uri == null || uri.scheme?.lowercase() !in setOf("http", "https") || uri.host.isNullOrBlank()) {
|
|
814
820
|
return done(CraftNativeCapabilityResult(code = "INVALID_ARGUMENT", message = "media.keep needs an absolute http or https URL"))
|
|
815
821
|
}
|
|
816
822
|
local(context, address)?.let { return done(CraftNativeCapabilityResult(data = describe(address, it))) }
|
|
823
|
+
if (!cellular && metered(context)) {
|
|
824
|
+
return done(CraftNativeCapabilityResult(data = JSONObject().put("ok", false).put("url", address).put("reason", "cellular")))
|
|
825
|
+
}
|
|
817
826
|
synchronized(inFlight) {
|
|
818
827
|
inFlight[address]?.let { waiting ->
|
|
819
828
|
waiting.add(done)
|
|
@@ -851,6 +860,12 @@ internal object CraftNativeMedia {
|
|
|
851
860
|
}
|
|
852
861
|
}
|
|
853
862
|
|
|
863
|
+
/** Whether the connection now is one the person pays for by the byte. */
|
|
864
|
+
private fun metered(context: Context): Boolean = runCatching {
|
|
865
|
+
val connectivity = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as android.net.ConnectivityManager
|
|
866
|
+
connectivity.isActiveNetworkMetered
|
|
867
|
+
}.getOrDefault(false)
|
|
868
|
+
|
|
854
869
|
/** Removes every copy whose address is not in `keep`; answers how many. */
|
|
855
870
|
fun prune(context: Context, keep: List<String>): Int {
|
|
856
871
|
val wanted = keep.map { file(context, it).name }.toSet()
|
package/dist/cli.js
CHANGED
|
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3303
3303
|
`);
|
|
3304
3304
|
}
|
|
3305
3305
|
// package.json
|
|
3306
|
-
var version = "0.0.
|
|
3306
|
+
var version = "0.0.171";
|
|
3307
3307
|
|
|
3308
3308
|
// bin/cli.ts
|
|
3309
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -647,6 +647,12 @@ enum CraftNativeActions {
|
|
|
647
647
|
/// offline, fetched once (a copy already there answers at once, and two
|
|
648
648
|
/// screens asking at the same time share one download). A refused answer
|
|
649
649
|
/// (not 2xx) keeps nothing and says its status. Cancelled like fetch.
|
|
650
|
+
///
|
|
651
|
+
/// `cellular: false` keeps it off a metered connection: iOS refuses the
|
|
652
|
+
/// download on cellular, a personal hotspot and Low Data Mode, and the
|
|
653
|
+
/// answer is `{ ok: false, reason: "cellular" }` straight away, so the
|
|
654
|
+
/// screen can say it is waiting for Wi-Fi instead of failing. A copy
|
|
655
|
+
/// already kept still answers, whatever the connection.
|
|
650
656
|
private static func keepMedia(
|
|
651
657
|
requestToken: String,
|
|
652
658
|
args: [Any],
|
|
@@ -675,12 +681,21 @@ enum CraftNativeActions {
|
|
|
675
681
|
request.setValue(value, forHTTPHeaderField: name)
|
|
676
682
|
}
|
|
677
683
|
}
|
|
684
|
+
if options["cellular"] as? Bool == false {
|
|
685
|
+
request.allowsExpensiveNetworkAccess = false
|
|
686
|
+
request.allowsConstrainedNetworkAccess = false
|
|
687
|
+
}
|
|
678
688
|
if CraftNativeMedia.join(address, { complete(requestToken, $0, completion) }) { return }
|
|
679
689
|
let task = fetchSession.downloadTask(with: request) { location, response, error in
|
|
680
690
|
cancellationLock.lock()
|
|
681
691
|
fetchTasks.removeValue(forKey: requestToken)
|
|
682
692
|
cancellationLock.unlock()
|
|
683
693
|
if let error = error {
|
|
694
|
+
// Refused only because the connection is metered: not a failure, a wait.
|
|
695
|
+
if let reason = (error as? URLError)?.networkUnavailableReason, reason == .expensive || reason == .constrained {
|
|
696
|
+
CraftNativeMedia.finish(address, .success(["ok": false, "url": address, "reason": "cellular"]))
|
|
697
|
+
return
|
|
698
|
+
}
|
|
684
699
|
let code = (error as? URLError)?.code == .timedOut ? "TIMEOUT" : "NETWORK_ERROR"
|
|
685
700
|
CraftNativeMedia.finish(address, failure(code, error.localizedDescription))
|
|
686
701
|
return
|
|
@@ -150,6 +150,11 @@ final class CraftNativeMutationDocument {
|
|
|
150
150
|
materialize(id, nodes: nodes)
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/// The committed parent of a node: nil for the root or a node it does not hold.
|
|
154
|
+
func parentId(of id: String) -> String? {
|
|
155
|
+
nodes[id]?.parent
|
|
156
|
+
}
|
|
157
|
+
|
|
153
158
|
private func createNode(
|
|
154
159
|
_ operation: [String: Any],
|
|
155
160
|
nodes: inout [String: Node],
|
|
@@ -291,8 +296,21 @@ final class CraftNativeMutationDocument {
|
|
|
291
296
|
return values.compactMap { $0 as? String }
|
|
292
297
|
}
|
|
293
298
|
|
|
299
|
+
/// Every type the renderer draws children of (CraftNativeScreen's
|
|
300
|
+
/// reconcile), by the names it accepts (`canonicalType`, so `svg` is
|
|
301
|
+
/// `Svg`). Only View, SafeAreaView and ScrollView used to be here, so a
|
|
302
|
+
/// drawing revealed after the first paint (an `:if` around an `<svg>`)
|
|
303
|
+
/// had its shapes refused as INVALID_PARENT, and the whole screen was
|
|
304
|
+
/// drawn again; Android's host already took the same list.
|
|
305
|
+
static let containerTypes: Set<String> = [
|
|
306
|
+
"View", "SafeAreaView", "ScrollView", "KeyboardAvoidingView", "Modal", "Pressable",
|
|
307
|
+
"TouchableOpacity", "TouchableHighlight", "ImageBackground", "FlatList", "SectionList",
|
|
308
|
+
"Fragment", "Svg", "G",
|
|
309
|
+
]
|
|
310
|
+
private static let containerTypeKeys = Set(containerTypes.map { $0.lowercased() })
|
|
311
|
+
|
|
294
312
|
private func acceptsChildren(_ type: String) -> Bool {
|
|
295
|
-
type
|
|
313
|
+
Self.containerTypeKeys.contains(type.lowercased())
|
|
296
314
|
}
|
|
297
315
|
|
|
298
316
|
private func descendants(of id: String, nodes: [String: Node]) -> Set<String> {
|
|
@@ -3816,7 +3816,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3816
3816
|
let focused = firstResponder(in: rootStack) ?? lastFocusedInput
|
|
3817
3817
|
var applied = Set<String>()
|
|
3818
3818
|
for id in ids {
|
|
3819
|
-
let target =
|
|
3819
|
+
guard let target = drawnTarget(id) else { return false }
|
|
3820
3820
|
guard applied.insert(target).inserted else { continue }
|
|
3821
3821
|
guard let document = mutationDocument.node(target), let previous = renderedNode(target) else { return false }
|
|
3822
3822
|
let next = reconcile(document, identity: previous.identity, path: target, previous: previous)
|
|
@@ -3828,6 +3828,21 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
3828
3828
|
return true
|
|
3829
3829
|
}
|
|
3830
3830
|
|
|
3831
|
+
/// The drawn node a change to `id` redraws: the list or drawing a row or
|
|
3832
|
+
/// shape belongs to, else the node itself. A node this batch created is
|
|
3833
|
+
/// not drawn yet (a drawing an `:if` just revealed, its shapes inserted
|
|
3834
|
+
/// into it), so its nearest drawn ancestor draws it, rather than the whole
|
|
3835
|
+
/// screen being reconciled again for want of it.
|
|
3836
|
+
private func drawnTarget(_ id: String) -> String? {
|
|
3837
|
+
var current: String? = id
|
|
3838
|
+
while let candidate = current {
|
|
3839
|
+
let target = flatListOwners[candidate] ?? svgOwners[candidate] ?? candidate
|
|
3840
|
+
if renderedNode(target) != nil { return target }
|
|
3841
|
+
current = mutationDocument.parentId(of: candidate)
|
|
3842
|
+
}
|
|
3843
|
+
return nil
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3831
3846
|
private func renderedNode(_ id: String, below node: RenderedNode? = nil) -> RenderedNode? {
|
|
3832
3847
|
guard let node = node ?? renderedRoot else { return nil }
|
|
3833
3848
|
if node.protocolId == id { return node }
|