craft-native 0.0.198 → 0.0.200
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.
|
@@ -917,7 +917,7 @@ internal object CraftNativeMedia {
|
|
|
917
917
|
}
|
|
918
918
|
|
|
919
919
|
fun local(context: Context, address: String): File? =
|
|
920
|
-
address.takeIf { it.isNotBlank() }?.let { file(context, it) }?.takeIf(
|
|
920
|
+
address.takeIf { it.isNotBlank() }?.let { file(context, it) }?.takeIf { it.isFile && it.length() > 0 }
|
|
921
921
|
|
|
922
922
|
private fun describe(address: String, file: File) = JSONObject().put("ok", true).put("url", address).put("path", file.absolutePath).put("size", file.length())
|
|
923
923
|
|
|
@@ -951,15 +951,24 @@ internal object CraftNativeMedia {
|
|
|
951
951
|
headers.keys().forEach { name -> connection.setRequestProperty(name, headers.optString(name)) }
|
|
952
952
|
try {
|
|
953
953
|
val status = connection.responseCode
|
|
954
|
-
if (status
|
|
954
|
+
if (status != 200) return@runCatching CraftNativeCapabilityResult(data = JSONObject().put("ok", false).put("url", address).put("status", status))
|
|
955
955
|
val target = file(context, address)
|
|
956
956
|
target.parentFile?.mkdirs()
|
|
957
957
|
// Written aside and moved into place, so a half-written file is never played.
|
|
958
|
-
val staging = File(target.parentFile, "${
|
|
959
|
-
|
|
960
|
-
|
|
958
|
+
val staging = File(target.parentFile, "${target.name}.part")
|
|
959
|
+
try {
|
|
960
|
+
val size = connection.inputStream.use { input -> staging.outputStream().use { input.copyTo(it) } }
|
|
961
|
+
val expected = connection.contentLengthLong
|
|
962
|
+
if (size <= 0 || (expected >= 0 && size != expected)) {
|
|
963
|
+
return@runCatching CraftNativeCapabilityResult(code = "NETWORK_ERROR", message = "media.keep received an incomplete file")
|
|
964
|
+
}
|
|
965
|
+
synchronized(inFlight) {
|
|
966
|
+
if (!staging.renameTo(target)) {
|
|
967
|
+
return@runCatching CraftNativeCapabilityResult(code = "IO_ERROR", message = "media.keep could not store the file")
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
} finally {
|
|
961
971
|
staging.delete()
|
|
962
|
-
return@runCatching CraftNativeCapabilityResult(code = "IO_ERROR", message = "media.keep could not store the file")
|
|
963
972
|
}
|
|
964
973
|
CraftNativeCapabilityResult(data = describe(address, target))
|
|
965
974
|
} finally {
|
|
@@ -983,7 +992,14 @@ internal object CraftNativeMedia {
|
|
|
983
992
|
fun prune(context: Context, keep: List<String>): Int {
|
|
984
993
|
val wanted = keep.map { file(context, it).name }.toSet()
|
|
985
994
|
var removed = 0
|
|
986
|
-
|
|
995
|
+
synchronized(inFlight) {
|
|
996
|
+
// A concurrent keep owns its target and staging file until it finishes.
|
|
997
|
+
val active = inFlight.keys.map { file(context, it).name }.toSet()
|
|
998
|
+
root(context).listFiles()?.forEach { file ->
|
|
999
|
+
val target = file.name.removeSuffix(".part")
|
|
1000
|
+
if (target !in active && file.name !in wanted && file.delete()) removed++
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
987
1003
|
return removed
|
|
988
1004
|
}
|
|
989
1005
|
|
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.200";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
|
@@ -309,7 +309,7 @@ function craftBinaryNotFoundMessage(triedPath) {
|
|
|
309
309
|
`);
|
|
310
310
|
}
|
|
311
311
|
// package.json
|
|
312
|
-
var version = "0.0.
|
|
312
|
+
var version = "0.0.200";
|
|
313
313
|
|
|
314
314
|
// src/sdk-version.ts
|
|
315
315
|
var SDK_VERSION = version;
|
|
@@ -10403,11 +10403,17 @@ ${e.stdout ?? ""}`;
|
|
|
10403
10403
|
};
|
|
10404
10404
|
}
|
|
10405
10405
|
async function verifyBundleTrust(appPath, policy = {}) {
|
|
10406
|
-
const identity = await readBundleIdentity(appPath);
|
|
10407
|
-
const requireNotarized = policy.requireNotarized ?? true;
|
|
10408
10406
|
if (!import_fs3.existsSync(appPath)) {
|
|
10409
|
-
return {
|
|
10407
|
+
return {
|
|
10408
|
+
ok: false,
|
|
10409
|
+
identity: { identifier: null, teamId: null, authority: null },
|
|
10410
|
+
gatekeeperSource: null,
|
|
10411
|
+
reason: "unreadable",
|
|
10412
|
+
detail: `No such bundle: ${appPath}`
|
|
10413
|
+
};
|
|
10410
10414
|
}
|
|
10415
|
+
const identity = await readBundleIdentity(appPath);
|
|
10416
|
+
const requireNotarized = policy.requireNotarized ?? true;
|
|
10411
10417
|
try {
|
|
10412
10418
|
await execFileAsync("codesign", ["--verify", "--deep", "--strict", "--verbose=2", appPath]);
|
|
10413
10419
|
} catch (error) {
|
package/dist/index.js
CHANGED
|
@@ -57,7 +57,7 @@ function craftBinaryNotFoundMessage(triedPath) {
|
|
|
57
57
|
`);
|
|
58
58
|
}
|
|
59
59
|
// package.json
|
|
60
|
-
var version = "0.0.
|
|
60
|
+
var version = "0.0.200";
|
|
61
61
|
|
|
62
62
|
// src/sdk-version.ts
|
|
63
63
|
var SDK_VERSION = version;
|
|
@@ -10162,11 +10162,17 @@ ${e.stdout ?? ""}`;
|
|
|
10162
10162
|
};
|
|
10163
10163
|
}
|
|
10164
10164
|
async function verifyBundleTrust(appPath, policy = {}) {
|
|
10165
|
-
const identity = await readBundleIdentity(appPath);
|
|
10166
|
-
const requireNotarized = policy.requireNotarized ?? true;
|
|
10167
10165
|
if (!existsSync4(appPath)) {
|
|
10168
|
-
return {
|
|
10166
|
+
return {
|
|
10167
|
+
ok: false,
|
|
10168
|
+
identity: { identifier: null, teamId: null, authority: null },
|
|
10169
|
+
gatekeeperSource: null,
|
|
10170
|
+
reason: "unreadable",
|
|
10171
|
+
detail: `No such bundle: ${appPath}`
|
|
10172
|
+
};
|
|
10169
10173
|
}
|
|
10174
|
+
const identity = await readBundleIdentity(appPath);
|
|
10175
|
+
const requireNotarized = policy.requireNotarized ?? true;
|
|
10170
10176
|
try {
|
|
10171
10177
|
await execFileAsync("codesign", ["--verify", "--deep", "--strict", "--verbose=2", appPath]);
|
|
10172
10178
|
} catch (error) {
|
|
@@ -1022,6 +1022,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1022
1022
|
private var tabBarHeight: CGFloat = 0
|
|
1023
1023
|
private var tabSwipeRecognizers: [UIGestureRecognizer] = []
|
|
1024
1024
|
private var activeTabSwipeTarget: String?
|
|
1025
|
+
private var tabSwipeSettling = false
|
|
1025
1026
|
/// The edge the incoming tab started from for the current gesture. Keep
|
|
1026
1027
|
/// this separate from the live frame: deriving it after the drag begins
|
|
1027
1028
|
/// makes the target jump when the translation crosses the midpoint.
|
|
@@ -1077,6 +1078,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1077
1078
|
}
|
|
1078
1079
|
|
|
1079
1080
|
@objc private func tabSwipeRecognized(_ recognizer: UIPanGestureRecognizer) {
|
|
1081
|
+
guard !tabSwipeSettling else { return }
|
|
1080
1082
|
let tabs = hybrid.tabIds
|
|
1081
1083
|
guard let index = tabs.firstIndex(of: currentTab), tabs.count > 1 else { return }
|
|
1082
1084
|
let translation = recognizer.translation(in: view).x
|
|
@@ -1101,9 +1103,19 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1101
1103
|
CraftChrome.shared.swipe(to: tabs[next])
|
|
1102
1104
|
return
|
|
1103
1105
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
+
// Keep the target until the settling animation has read it and
|
|
1107
|
+
// finished. Clearing it here made finishInteractiveTabSwipe return
|
|
1108
|
+
// early, leaving cancelled screens offset and completed swipes
|
|
1109
|
+
// jumping straight to their destination.
|
|
1110
|
+
tabSwipeSettling = true
|
|
1111
|
+
let direction: CGFloat = tabSwipeFrom > 0 ? -1 : 1
|
|
1112
|
+
let progress = translation * direction
|
|
1113
|
+
let speed = velocity * direction
|
|
1114
|
+
let complete = recognizer.state == .ended && speed > -700
|
|
1115
|
+
&& (progress >= max(32, view.bounds.width * 0.28) || speed > 700)
|
|
1106
1116
|
finishInteractiveTabSwipe(completed: complete) { [weak self] in
|
|
1117
|
+
self?.activeTabSwipeTarget = nil
|
|
1118
|
+
self?.tabSwipeSettling = false
|
|
1107
1119
|
guard complete else {
|
|
1108
1120
|
CraftChrome.shared.cancelTabSwipe()
|
|
1109
1121
|
return
|
|
@@ -1121,8 +1133,19 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1121
1133
|
/// transparent until its page route catches up. Two web roots still use
|
|
1122
1134
|
/// the route-aware fallback because one WebView cannot render both pages.
|
|
1123
1135
|
private func beginInteractiveTabSwipe(to id: String, direction: CGFloat) -> Bool {
|
|
1124
|
-
guard let current = visibleStack,
|
|
1125
|
-
|
|
1136
|
+
guard let current = visibleStack, view.bounds.width > 0 else { return false }
|
|
1137
|
+
let target: UINavigationController
|
|
1138
|
+
if let mounted = stacks[id] {
|
|
1139
|
+
target = mounted
|
|
1140
|
+
} else {
|
|
1141
|
+
// A native tab must also follow the first swipe to it. Do not
|
|
1142
|
+
// create a second web root here: the shared WebView cannot draw
|
|
1143
|
+
// both pages during the same gesture.
|
|
1144
|
+
guard hybrid.routes.match(id) != nil else { return false }
|
|
1145
|
+
target = makeStack(tab: id, rootPath: id)
|
|
1146
|
+
(target.topViewController as? CraftNativeScreenController)?.screenVisibilityChanged(false)
|
|
1147
|
+
}
|
|
1148
|
+
guard current.view !== target.view,
|
|
1126
1149
|
!(current.topViewController is CraftHybridWebSlot && target.topViewController is CraftHybridWebSlot),
|
|
1127
1150
|
view.bounds.width > 0 else { return false }
|
|
1128
1151
|
if let slot = target.topViewController as? CraftHybridWebSlot {
|
|
@@ -1155,7 +1178,11 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1155
1178
|
guard let targetId = activeTabSwipeTarget, let current = visibleStack,
|
|
1156
1179
|
let target = stacks[targetId] else { return }
|
|
1157
1180
|
let width = max(1, view.bounds.width)
|
|
1158
|
-
|
|
1181
|
+
// Reversing past the start stays at zero progress, rather than
|
|
1182
|
+
// pulling both screens away and exposing the background.
|
|
1183
|
+
let offset = tabSwipeFrom > 0
|
|
1184
|
+
? min(0, max(-width, translation))
|
|
1185
|
+
: max(0, min(width, translation))
|
|
1159
1186
|
target.view.frame.origin.x = tabSwipeFrom + offset
|
|
1160
1187
|
current.view.frame.origin.x = offset
|
|
1161
1188
|
}
|
|
@@ -1164,14 +1191,19 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1164
1191
|
guard let targetId = activeTabSwipeTarget, let current = visibleStack,
|
|
1165
1192
|
let target = stacks[targetId] else { completion(); return }
|
|
1166
1193
|
let width = view.bounds.width
|
|
1167
|
-
let direction: CGFloat =
|
|
1194
|
+
let direction: CGFloat = tabSwipeFrom > 0 ? 1 : -1
|
|
1168
1195
|
let end = completed ? 0 : direction * width
|
|
1169
|
-
let
|
|
1170
|
-
|
|
1196
|
+
let settlingDuration = completed ? 0.22 : 0.18
|
|
1197
|
+
let duration = CraftChrome.motion(.easeOut(duration: settlingDuration)) == nil ? 0 : settlingDuration
|
|
1198
|
+
UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut, .beginFromCurrentState], animations: {
|
|
1171
1199
|
target.view.frame.origin.x = end
|
|
1172
1200
|
current.view.frame.origin.x = completed ? -direction * width : 0
|
|
1173
1201
|
}, completion: { _ in
|
|
1174
|
-
if
|
|
1202
|
+
if completed {
|
|
1203
|
+
current.view.isHidden = true
|
|
1204
|
+
current.view.frame = self.view.bounds
|
|
1205
|
+
target.view.frame = self.view.bounds
|
|
1206
|
+
} else {
|
|
1175
1207
|
target.view.isHidden = true
|
|
1176
1208
|
target.view.frame = self.view.bounds
|
|
1177
1209
|
current.view.frame = self.view.bounds
|
|
@@ -1690,7 +1722,8 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1690
1722
|
|
|
1691
1723
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
|
1692
1724
|
if tabSwipeRecognizers.contains(where: { $0 === gestureRecognizer }) {
|
|
1693
|
-
guard
|
|
1725
|
+
guard activeTabSwipeTarget == nil, !tabSwipeSettling,
|
|
1726
|
+
hybrid.tabIds.count > 1, let nav = visibleStack, let top = nav.topViewController else { return false }
|
|
1694
1727
|
guard CraftChrome.shared.tabSwipeEnabled else { return false }
|
|
1695
1728
|
// Only between the tabs' own first screens, as the bar shows them:
|
|
1696
1729
|
// deeper in a tab a swipe to the right is Back, and a screen that
|
|
@@ -515,7 +515,7 @@ enum CraftNativeMedia {
|
|
|
515
515
|
static func local(_ address: String) -> URL? {
|
|
516
516
|
guard !address.isEmpty else { return nil }
|
|
517
517
|
let url = file(for: address)
|
|
518
|
-
return
|
|
518
|
+
return ((try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0) > 0 ? url : nil
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
static func describe(_ address: String, _ url: URL) -> [String: Any] {
|
|
@@ -545,11 +545,23 @@ enum CraftNativeMedia {
|
|
|
545
545
|
/// A finished download moved into place (atomically, so a half-written
|
|
546
546
|
/// file is never played), or a refusal with the server's status.
|
|
547
547
|
static func store(_ address: String, at location: URL, response: HTTPURLResponse) -> Result<Any, CraftNativeActionError> {
|
|
548
|
-
guard
|
|
548
|
+
guard response.statusCode == 200 else {
|
|
549
549
|
return .success(["ok": false, "url": address, "status": response.statusCode])
|
|
550
550
|
}
|
|
551
551
|
let fm = FileManager.default
|
|
552
|
+
// Commit and prune share a lock; neither can remove a file being committed.
|
|
553
|
+
lock.lock()
|
|
554
|
+
defer { lock.unlock() }
|
|
555
|
+
let staging = root.appendingPathComponent(UUID().uuidString + ".part")
|
|
556
|
+
defer { try? fm.removeItem(at: staging) }
|
|
552
557
|
do {
|
|
558
|
+
let size = (try location.resourceValues(forKeys: [.fileSizeKey])).fileSize ?? 0
|
|
559
|
+
// URLSession normally rejects a truncated transfer; also guard this boundary.
|
|
560
|
+
let expected = response.expectedContentLength
|
|
561
|
+
let encoded = response.value(forHTTPHeaderField: "Content-Encoding")
|
|
562
|
+
if size <= 0 || (encoded == nil && expected >= 0 && Int64(size) != expected) {
|
|
563
|
+
return .failure(CraftNativeActionError(code: "NETWORK_ERROR", message: "media.keep received an incomplete file"))
|
|
564
|
+
}
|
|
553
565
|
var folder = root
|
|
554
566
|
if !fm.fileExists(atPath: folder.path) {
|
|
555
567
|
try fm.createDirectory(at: folder, withIntermediateDirectories: true)
|
|
@@ -558,7 +570,6 @@ enum CraftNativeMedia {
|
|
|
558
570
|
try? folder.setResourceValues(values)
|
|
559
571
|
}
|
|
560
572
|
let target = file(for: address)
|
|
561
|
-
let staging = root.appendingPathComponent(UUID().uuidString + ".part")
|
|
562
573
|
try fm.moveItem(at: location, to: staging)
|
|
563
574
|
_ = try? fm.removeItem(at: target)
|
|
564
575
|
try fm.moveItem(at: staging, to: target)
|
|
@@ -571,10 +582,13 @@ enum CraftNativeMedia {
|
|
|
571
582
|
|
|
572
583
|
/// Removes every copy whose address is not in `keep`; answers how many.
|
|
573
584
|
static func prune(keep: [String]) -> Int {
|
|
585
|
+
lock.lock()
|
|
586
|
+
defer { lock.unlock() }
|
|
574
587
|
let wanted = Set(keep.map { file(for: $0).lastPathComponent })
|
|
575
588
|
let files = (try? FileManager.default.contentsOfDirectory(at: root, includingPropertiesForKeys: nil)) ?? []
|
|
576
589
|
var removed = 0
|
|
577
|
-
|
|
590
|
+
let active = Set(inFlight.keys.map { file(for: $0).lastPathComponent })
|
|
591
|
+
for url in files where !wanted.contains(url.lastPathComponent) && !active.contains(url.lastPathComponent) {
|
|
578
592
|
if (try? FileManager.default.removeItem(at: url)) != nil { removed += 1 }
|
|
579
593
|
}
|
|
580
594
|
return removed
|