craft-native 0.0.497 → 0.0.500

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.
@@ -3067,6 +3067,19 @@ internal class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(cont
3067
3067
  }
3068
3068
  }
3069
3069
 
3070
+ /**
3071
+ * How far the grouped tab actions reach over the end of this scroll view,
3072
+ * in pixels (#523). Its content gets that much room at the end, so the
3073
+ * last of it can be brought clear of them; the rest still scrolls behind
3074
+ * them, in the viewport it had.
3075
+ */
3076
+ fun setFloatingCover(cover: Int) {
3077
+ val next = if (horizontalMode == true) 0 else max(0, cover)
3078
+ if (vertical.paddingBottom == next) return
3079
+ vertical.clipToPadding = false
3080
+ vertical.setPadding(vertical.paddingLeft, vertical.paddingTop, vertical.paddingRight, next)
3081
+ }
3082
+
3070
3083
  private var bounces = true
3071
3084
  private var alwaysBounceVertical = true
3072
3085
  private var alwaysBounceHorizontal = true
@@ -3585,6 +3598,17 @@ private class CraftNativeTextField(context: Context) : EditText(context) {
3585
3598
  CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
3586
3599
 
3587
3600
  companion object { val CLEAR_ACTION_ID = View.generateViewId() }
3601
+ /**
3602
+ * `autoFocus` as this field was last drawn. Focus is taken when it turns
3603
+ * on, which mounting with it on is, and not on each render while it stays
3604
+ * on: that took the keyboard back whenever anything else changed (#511).
3605
+ */
3606
+ private var autoFocus = false
3607
+ fun autoFocusTurnedOn(wanted: Boolean): Boolean {
3608
+ val turnedOn = wanted && !autoFocus
3609
+ autoFocus = wanted
3610
+ return turnedOn
3611
+ }
3588
3612
  var clearButtonMode = "never"
3589
3613
  var onClearRequest: (() -> Unit)? = null
3590
3614
  private var clearPressed = false
@@ -5635,7 +5659,8 @@ class MainActivity : AppCompatActivity() {
5635
5659
  if (activeScreen.active) send(activeScreen, "SOUND_EVENT", payload)
5636
5660
  }
5637
5661
  screen = activeScreen
5638
- CraftNativeHub.register(activeScreen.capabilityScope) { type, payload ->
5662
+ // Not able to hear yet: its prelude is built and run below.
5663
+ CraftNativeHub.registerWaiting(activeScreen.capabilityScope) { type, payload ->
5639
5664
  if (activeScreen.active) send(activeScreen, type, payload)
5640
5665
  }
5641
5666
  if (!headless) {
@@ -5845,6 +5870,9 @@ class MainActivity : AppCompatActivity() {
5845
5870
  if (!activeScreen.active) return@addListener
5846
5871
  try {
5847
5872
  setup.get()
5873
+ // The prelude has run, so the isolate can hear: what changed
5874
+ // while it was being built, before the bundle reads any of it.
5875
+ CraftNativeHub.ready(activeScreen.capabilityScope)
5848
5876
  val source = CraftNativeBundle.source(this, route)
5849
5877
  if (source == null) {
5850
5878
  if (activeScreen.active) showError("Missing native-screen.js. Build with --native-bundle after compiling STX.")
@@ -5891,6 +5919,8 @@ class MainActivity : AppCompatActivity() {
5891
5919
  }
5892
5920
  }, executor)
5893
5921
  } catch (error: Exception) {
5922
+ // Nothing will run in this isolate, so nothing is kept for it.
5923
+ CraftNativeHub.ready(activeScreen.capabilityScope)
5894
5924
  Log.e(TAG, "Native STX bridge failed", error)
5895
5925
  showError("Native STX bridge failed: ${error.message}")
5896
5926
  }
@@ -6796,7 +6826,7 @@ class MainActivity : AppCompatActivity() {
6796
6826
  send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("text", text.toString())))
6797
6827
  true
6798
6828
  }
6799
- if (craftNativeBoolean(props.opt("autoFocus")) && !isFocused) post {
6829
+ if (autoFocusTurnedOn(craftNativeBoolean(props.opt("autoFocus"))) && !isFocused) post {
6800
6830
  if (!isFocused) {
6801
6831
  requestFocus()
6802
6832
  (getSystemService(INPUT_METHOD_SERVICE) as? android.view.inputmethod.InputMethodManager)
@@ -8723,9 +8753,13 @@ class MainActivity : AppCompatActivity() {
8723
8753
  }
8724
8754
  "pause" -> completion(CraftNativeCapabilityResult(data = screen.sounds.pause(id)))
8725
8755
  "resume" -> completion(CraftNativeCapabilityResult(data = screen.sounds.resume(id)))
8726
- "setVolume" -> completion(CraftNativeCapabilityResult(data = screen.sounds.setVolume(id, args.optDouble(1, Double.NaN), args.optJSONObject(2)?.optDouble("fade", 0.0) ?: 0.0)))
8727
- "stop" -> completion(CraftNativeCapabilityResult(data = screen.sounds.stop(id, args.optJSONObject(1)?.optDouble("fadeOut", 0.0) ?: 0.0)))
8728
- "stopAll" -> completion(CraftNativeCapabilityResult(data = screen.sounds.stopAll(args.optJSONObject(0)?.optDouble("fadeOut", 0.0) ?: 0.0)))
8756
+ // A number is a JSON number: `optDouble` also read "0.5" as one,
8757
+ // where UIKit answers false for a volume given as text.
8758
+ "setVolume" -> completion(CraftNativeCapabilityResult(data = screen.sounds.setVolume(id,
8759
+ (args.opt(1) as? Number)?.toDouble() ?: Double.NaN,
8760
+ (args.optJSONObject(2)?.opt("fade") as? Number)?.toDouble() ?: 0.0)))
8761
+ "stop" -> completion(CraftNativeCapabilityResult(data = screen.sounds.stop(id, screen.sounds.fadeOut(args.optJSONObject(1)))))
8762
+ "stopAll" -> completion(CraftNativeCapabilityResult(data = screen.sounds.stopAll(screen.sounds.fadeOut(args.optJSONObject(0)))))
8729
8763
  else -> completion(CraftNativeCapabilityResult(code = "UNKNOWN_ACTION", message = "Unsupported Audio method"))
8730
8764
  }
8731
8765
  }
@@ -8865,15 +8899,8 @@ class MainActivity : AppCompatActivity() {
8865
8899
  else -> CraftNativeCapabilityResult(code = "UNKNOWN_ACTION", message = "Unsupported native API")
8866
8900
  }
8867
8901
 
8868
- internal fun deepLinkData(uri: android.net.Uri): JSONObject = JSONObject()
8869
- .put("url", uri.toString())
8870
- .put("scheme", uri.scheme ?: "")
8871
- .put("host", uri.host ?: "")
8872
- .put("path", uri.path ?: "")
8873
- .put("query", uri.query ?: "")
8874
- .put("queryParams", JSONObject().apply {
8875
- uri.queryParameterNames.forEach { name -> put(name, uri.getQueryParameter(name) ?: "") }
8876
- })
8902
+ /** A link as the screens hear it: `CraftAppLinks.describe`, which the page bridge uses too. */
8903
+ internal fun deepLinkData(uri: android.net.Uri): JSONObject = CraftAppLinks.describe(uri)
8877
8904
 
8878
8905
  /** App-wide news (the app's state, a link): every screen and the app script hear it. */
8879
8906
  private fun publish(type: String, payload: JSONObject) {
@@ -9423,6 +9450,7 @@ class MainActivity : AppCompatActivity() {
9423
9450
  screen.capabilityTimeouts.clear()
9424
9451
  screen.pendingCapabilities.forEach(capabilities::cancel)
9425
9452
  screen.pendingCapabilities.clear()
9453
+ capabilities.endScope(screen.capabilityScope)
9426
9454
  releaseViewState(screen, retainedViews(screen).toList())
9427
9455
  screen.inputWatchers.forEach { (input, watcher) -> input.removeTextChangedListener(watcher) }
9428
9456
  screen.inputWatchers.clear()
@@ -9463,6 +9491,8 @@ class MainActivity : AppCompatActivity() {
9463
9491
  screen.capabilityTimeouts.clear()
9464
9492
  screen.pendingCapabilities.forEach(capabilities::cancel)
9465
9493
  screen.pendingCapabilities.clear()
9494
+ // Again: a transaction can be begun while the isolate is closing.
9495
+ capabilities.endScope(screen.capabilityScope)
9466
9496
  done()
9467
9497
  }
9468
9498
  }
@@ -60,7 +60,6 @@ export declare function exec(command: string, options?: ExecOptions): Promise<Ex
60
60
  */
61
61
  export interface SpawnOptions {
62
62
  cwd?: string;
63
- env?: Record<string, string>;
64
63
  /**
65
64
  * Run `command` as a command line through a shell (cmd.exe on Windows,
66
65
  * /bin/sh elsewhere) instead of executing it directly.
@@ -74,45 +73,41 @@ export interface SpawnOptions {
74
73
  */
75
74
  shell?: boolean;
76
75
  }
76
+ /**
77
+ * A process started through the native bridge.
78
+ *
79
+ * What native `shell.spawn` does, and so all this class offers: start a
80
+ * program, and kill it. The child inherits Craft's own standard output and
81
+ * error. Nothing it writes is delivered to the page, and its exit is not
82
+ * reported.
83
+ *
84
+ * `onStdout`, `onStderr`, `onExit` and `write` used to be declared here. No
85
+ * native bridge ever emitted the events behind them or accepted input, so a
86
+ * handler registered with them was never called. They are removed rather than
87
+ * left as methods that accept a callback and stay silent. To read a child's
88
+ * output, run it in your app's backend process and send the result to the
89
+ * page; `exec()` covers a command that finishes and returns its output whole.
90
+ *
91
+ * Windows and Linux desktop builds do not serve the `shell` namespace at all:
92
+ * there `start()` rejects.
93
+ */
77
94
  export declare class ChildProcess {
78
95
  private command;
79
96
  private args;
80
97
  private options;
81
98
  private processId;
82
- private onStdoutHandlers;
83
- private onStderrHandlers;
84
- private onExitHandlers;
85
- private domListeners;
86
- private exited;
87
99
  constructor(command: string, args?: string[], options?: SpawnOptions);
88
- private addDomListener;
89
- private removeDomListeners;
90
- /** Pid assigned by the native bridge, or null before start(). */
100
+ /** Identifier this process is registered under, or null before start(). */
91
101
  get pid(): string | null;
92
102
  /**
93
- * Start the process
103
+ * Start the process. Resolves once native has started it.
94
104
  */
95
105
  start(): Promise<void>;
96
106
  /**
97
- * Write to stdin
98
- */
99
- write(data: string): Promise<void>;
100
- /**
101
- * Kill the process
102
- */
103
- kill(signal?: string): Promise<void>;
104
- /**
105
- * Listen for stdout data
106
- */
107
- onStdout(handler: (data: string) => void): () => void;
108
- /**
109
- * Listen for stderr data
110
- */
111
- onStderr(handler: (data: string) => void): () => void;
112
- /**
113
- * Listen for exit
107
+ * Kill the process. Native sends the platform's default termination; there
108
+ * is no signal to choose.
114
109
  */
115
- onExit(handler: (code: number) => void): () => void;
110
+ kill(): Promise<void>;
116
111
  }
117
112
  /**
118
113
  * Spawn a child process
@@ -46,6 +46,12 @@ export declare const BridgeErrorCodes: {
46
46
  readonly PROTOCOL_MISMATCH: -8;
47
47
  /** Bridge transitioned from connected to disconnected with requests in flight */
48
48
  readonly DISCONNECTED: -9;
49
+ /**
50
+ * The native side received the request and refused or failed it. Not
51
+ * retried: asking again gets the same answer. `data.nativeCode` carries
52
+ * the bridge's own string code when it sent one.
53
+ */
54
+ readonly NATIVE_ERROR: -10;
49
55
  };
50
56
  export type BridgeErrorCode = typeof BridgeErrorCodes[keyof typeof BridgeErrorCodes];
51
57
  /**
@@ -136,12 +142,12 @@ export declare class NativeBridge extends EventEmitter {
136
142
  * BridgeError(PROTOCOL_MISMATCH) if the native side reports a version we
137
143
  * don't speak.
138
144
  *
139
- * This is **opt-in** — you must call it explicitly. It assumes the host
140
- * registers a `_handshake` handler; the Zig core's
141
- * `Bridge.registerDefaults()` does this for you. If you're embedding Craft
142
- * inside a host that hasn't wired `_handshake`, do not call this method
143
- * (the timeout path will reject with TIMEOUT, which is louder than a
144
- * silent mismatch but still a failure).
145
+ * This is **opt-in** — you must call it explicitly, and only against a
146
+ * host of your own that answers a `_handshake` request. Craft's own
147
+ * webview hosts do not: inside one, requests go through
148
+ * `window.craft.invoke`, which takes `namespace.action` names, and this
149
+ * call is rejected immediately instead of negotiating anything. Use
150
+ * `window.craft.capabilities` to ask a Craft host what it serves.
145
151
  */
146
152
  handshake(): Promise<{
147
153
  nativeVersion: number;
@@ -203,6 +209,24 @@ export declare class NativeBridge extends EventEmitter {
203
209
  * use that to fail fast instead of waiting for the request timeout.
204
210
  */
205
211
  private send;
212
+ /**
213
+ * Hand a request to `window.craft.invoke`, the dispatcher that
214
+ * `craft-bridge.js` injects into every Craft webview, and settle the
215
+ * pending entry with what native answers.
216
+ *
217
+ * This is the only path by which `request()` reaches native code. The
218
+ * `{id, type: 'request', method, params}` envelope this class builds is not
219
+ * something any Craft host parses: posted raw to the `craft` message
220
+ * handler it was dropped, and every `request()` waited out its timeout and
221
+ * its retries. The method name is used as written, `namespace.action`, so
222
+ * it has to be one the native bridge serves (`fs.readFile`,
223
+ * `shell.openUrl`, `dialog.openFile`); one it does not serve is rejected
224
+ * with `NATIVE_ERROR` straight away.
225
+ *
226
+ * Returns false when the injected bridge is not present, so the older
227
+ * transports below still get their turn in a host that provides one.
228
+ */
229
+ private sendThroughInjectedBridge;
206
230
  /**
207
231
  * Reject the pending entry corresponding to `message.id` with `error`,
208
232
  * if any. Used by `send()` when serialization or transport throws so
package/dist/browser.js CHANGED
@@ -909,8 +909,24 @@ async function createBrowserPlatform(options = {}) {
909
909
  }
910
910
  if (method === "write") {
911
911
  const settings = object(args[2]);
912
- const contents = settings.encoding === "base64" ? Uint8Array.from(atob(String(args[1])), (letter) => letter.charCodeAt(0)) : String(args[1]);
913
- return file([contents], String(args[0]), String(settings.type ?? "application/octet-stream"));
912
+ const [name, data] = args;
913
+ const refuse = (message) => Object.assign(new Error(message), { code: "INVALID_ARGUMENT" });
914
+ if (typeof name !== "string" || !name.trim())
915
+ throw refuse("files.write needs a file name");
916
+ if (typeof data !== "string")
917
+ throw refuse("files.write needs its contents as a string");
918
+ const encoding = settings.encoding ?? "utf8";
919
+ if (encoding !== "utf8" && encoding !== "base64")
920
+ throw refuse("files.write encoding is 'utf8' or 'base64'");
921
+ let contents = data;
922
+ if (encoding === "base64") {
923
+ try {
924
+ contents = Uint8Array.from(atob(data), (letter) => letter.charCodeAt(0));
925
+ } catch {
926
+ throw refuse("files.write base64 is invalid");
927
+ }
928
+ }
929
+ return file([contents], name, String(settings.type ?? "application/octet-stream"));
914
930
  }
915
931
  if (method === "remove") {
916
932
  const path = String(args[0]);
@@ -1071,14 +1087,15 @@ async function createBrowserPlatform(options = {}) {
1071
1087
  throw error;
1072
1088
  }
1073
1089
  }
1090
+ const fadeOut = (options) => typeof options.fadeOut === "number" ? options.fadeOut : 0;
1074
1091
  if (method === "stopAll") {
1075
1092
  soundRequests.clear();
1076
- await Promise.all([...sounds.keys()].map((key) => stopSound(key, Number(first.fadeOut ?? 0))));
1093
+ await Promise.all([...sounds.keys()].map((key) => stopSound(key, fadeOut(first))));
1077
1094
  return true;
1078
1095
  }
1079
1096
  if (method === "stop") {
1080
1097
  soundRequests.delete(id);
1081
- await stopSound(id, Number(object(args[1]).fadeOut ?? 0));
1098
+ await stopSound(id, fadeOut(object(args[1])));
1082
1099
  return Boolean(sound);
1083
1100
  }
1084
1101
  if (!sound)
@@ -1092,7 +1109,12 @@ async function createBrowserPlatform(options = {}) {
1092
1109
  return true;
1093
1110
  }
1094
1111
  if (method === "setVolume") {
1095
- await fade(sound, Number(args[1]), Number(object(args[2]).fade ?? 0));
1112
+ const volume = args[1];
1113
+ const asked = object(args[2]).fade;
1114
+ const milliseconds = typeof asked === "number" ? asked : 0;
1115
+ if (typeof volume !== "number" || !Number.isFinite(volume) || !Number.isFinite(milliseconds) || milliseconds < 0)
1116
+ return false;
1117
+ await fade(sound, volume, milliseconds);
1096
1118
  return true;
1097
1119
  }
1098
1120
  }
package/dist/cli.js CHANGED
@@ -1395,6 +1395,33 @@ Description: ${description || opts.name}
1395
1395
  }
1396
1396
  });
1397
1397
  }
1398
+ function rpmSpec(options) {
1399
+ return [
1400
+ "%global debug_package %{nil}",
1401
+ "%global _build_id_links none",
1402
+ "%global __strip /bin/true",
1403
+ "%global __os_install_post %{nil}",
1404
+ `Name: ${options.name}`,
1405
+ `Version: ${options.version}`,
1406
+ "Release: 1",
1407
+ `Summary: ${options.summary}`,
1408
+ "License: MIT",
1409
+ `Source0: ${options.name}`,
1410
+ ...options.requires ? [`Requires: ${options.requires}`] : [],
1411
+ "",
1412
+ "%description",
1413
+ options.description,
1414
+ "",
1415
+ "%install",
1416
+ "mkdir -p %{buildroot}/usr/bin",
1417
+ `install -m 755 %{SOURCE0} %{buildroot}/usr/bin/${options.name}`,
1418
+ "",
1419
+ "%files",
1420
+ `/usr/bin/${options.name}`,
1421
+ ""
1422
+ ].join(`
1423
+ `);
1424
+ }
1398
1425
  async function createRPM(opts) {
1399
1426
  return new Promise((resolve) => {
1400
1427
  try {
@@ -1427,26 +1454,17 @@ async function createRPM(opts) {
1427
1454
  return r;
1428
1455
  }).join(", ");
1429
1456
  copyFileSync(opts.binaryPath, join3(sourcesDir, sanitizedName));
1430
- const specContent = `Name: ${sanitizedName}
1431
- Version: ${opts.version}
1432
- Release: 1%{?dist}
1433
- Summary: ${summary}
1434
- License: MIT
1435
- Requires: ${requires}
1436
-
1437
- %description
1438
- ${description}
1439
-
1440
- %install
1441
- mkdir -p %{buildroot}/usr/bin
1442
- install -m 755 %{SOURCE0} %{buildroot}/usr/bin/${sanitizedName}
1443
-
1444
- %files
1445
- /usr/bin/${sanitizedName}
1446
- `;
1457
+ const specContent = rpmSpec({ name: sanitizedName, version: opts.version, summary, description, requires });
1447
1458
  const specPath = join3(specDir, `${opts.name.toLowerCase()}.spec`);
1448
1459
  writeFileSync4(specPath, specContent);
1449
1460
  const proc = spawn("rpmbuild", ["-bb", specPath]);
1461
+ let said = "";
1462
+ proc.stdout?.on("data", (chunk) => {
1463
+ said += chunk.toString();
1464
+ });
1465
+ proc.stderr?.on("data", (chunk) => {
1466
+ said += chunk.toString();
1467
+ });
1450
1468
  proc.on("close", (code) => {
1451
1469
  if (code === 0) {
1452
1470
  const rpmName = `${opts.name.toLowerCase()}-${opts.version}-1.x86_64.rpm`;
@@ -1458,7 +1476,9 @@ install -m 755 %{SOURCE0} %{buildroot}/usr/bin/${sanitizedName}
1458
1476
  resolve({ success: false, error: "Failed to copy built RPM" });
1459
1477
  }
1460
1478
  } else {
1461
- resolve({ success: false, error: `rpmbuild exited with code ${code}` });
1479
+ resolve({ success: false, error: `rpmbuild exited with code ${code}: ${said.trim().split(`
1480
+ `).slice(-12).join(`
1481
+ `)}` });
1462
1482
  }
1463
1483
  });
1464
1484
  proc.on("error", (err) => {
@@ -3246,7 +3266,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3246
3266
  `);
3247
3267
  }
3248
3268
  // package.json
3249
- var version = "0.0.497";
3269
+ var version = "0.0.500";
3250
3270
 
3251
3271
  // bin/cli.ts
3252
3272
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -801,7 +801,7 @@ export interface TextInputProps extends BaseProps {
801
801
  autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters';
802
802
  /** Auto correct */
803
803
  autoCorrect?: boolean;
804
- /** Auto focus */
804
+ /** Focus the input when it mounts, or when this turns on; not on each render. */
805
805
  autoFocus?: boolean;
806
806
  /** Secure text entry (password) */
807
807
  secureTextEntry?: boolean;