@vestra-player/sdk 1.5.17 → 1.5.20

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/plugin/ui.js CHANGED
@@ -5,7 +5,7 @@ import { readFileSync } from "node:fs";
5
5
  import { basename, extname, resolve } from "node:path";
6
6
  import { failure, Latest, quiet } from "./runtime.js";
7
7
  import { dropsOf } from "./drops.js";
8
- import { MarkdownHandle, markdown } from "./markdown.js";
8
+ import { MarkdownHandle, markdown, remoteImageFile } from "./markdown.js";
9
9
  /** Slint names are kebab-case; plugin code may use camelCase. */
10
10
  export const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
11
11
  const encode = (props) => Object.fromEntries(Object.entries(props).map(([k, v]) => [kebab(k), v]));
@@ -21,51 +21,69 @@ export class ImageHandle {
21
21
  __vestraImage = true;
22
22
  #scope;
23
23
  #ref = ++images;
24
- constructor(scope, path) {
24
+ #maxSide;
25
+ constructor(scope, path, maxSide) {
25
26
  this.#scope = scope;
26
27
  this.path = path;
28
+ this.#maxSide = maxSide;
27
29
  }
28
30
  /** Released with its scope: writing it is `invalid`. */
29
31
  get released() {
30
32
  return this.#scope.disposed;
31
33
  }
32
34
  toJSON() {
33
- return { $vestraImage: this.path, ref: this.#ref };
35
+ // `maxSide` only when given: players before 0.5.1 refuse the field.
36
+ return this.#maxSide === undefined
37
+ ? { $vestraImage: this.path, ref: this.#ref }
38
+ : { $vestraImage: this.path, ref: this.#ref, maxSide: this.#maxSide };
34
39
  }
35
40
  }
41
+ /** `ImageOptions.maxSide`, checked: an integer 1–4096 or absent. */
42
+ export function imageSide(options) {
43
+ const side = options?.maxSide;
44
+ if (side === undefined)
45
+ return undefined;
46
+ if (!Number.isInteger(side) || side < 1 || side > 4096)
47
+ throw failure("invalid", "maxSide must be an integer 1–4096");
48
+ return side;
49
+ }
36
50
  /**
37
- * A property value as sent: images and documents in wire form, documents at
38
- * any depth (released ones refused wherever they are); an image only as the
39
- * whole value, like the player.
51
+ * A property value as sent: images and documents in wire form, at any depth
52
+ * (released ones refused wherever they are), like the player.
40
53
  */
41
- function wireValue(value, nested = false) {
54
+ function wireValue(value) {
42
55
  if (value instanceof MarkdownHandle) {
43
56
  if (value.released)
44
57
  throw failure("invalid", "Markdown document was released with the scope that created it");
45
58
  return value.toJSON();
46
59
  }
47
60
  if (value instanceof ImageHandle) {
48
- if (nested)
49
- throw failure("invalid", `image ${value.path}: an image reference must be the whole value of an image property`);
50
61
  if (value.released)
51
62
  throw failure("invalid", `image ${value.path} was released with the scope that created it`);
52
63
  return value.toJSON();
53
64
  }
54
65
  if (Array.isArray(value))
55
- return value.map((item) => wireValue(item, true));
66
+ return value.map((item) => wireValue(item));
56
67
  if (value !== null && typeof value === "object") {
57
68
  const out = {};
58
69
  for (const [key, item] of Object.entries(value))
59
- out[key] = wireValue(item, true);
70
+ out[key] = wireValue(item);
60
71
  return out;
61
72
  }
62
73
  return value;
63
74
  }
64
75
  export const ui = (scope) => ({
65
- image(path) {
76
+ image(path, options) {
66
77
  if (typeof path !== "string" || !path)
67
78
  throw failure("invalid", "ui.image needs a file path");
68
- return new ImageHandle(scope, path);
79
+ return new ImageHandle(scope, path, imageSide(options));
80
+ },
81
+ async remoteImage(url, options) {
82
+ if (typeof url !== "string" || !url.startsWith("https://"))
83
+ throw failure("invalid", "ui.remoteImage needs an https: address");
84
+ const side = imageSide(options);
85
+ const file = await remoteImageFile(scope.runtime, url);
86
+ return new ImageHandle(scope, file, side);
69
87
  },
70
88
  markdown: (text, options) => markdown(scope, text, options),
71
89
  });
@@ -46,6 +46,21 @@ export declare const UI_VALUE_LIMITS: {
46
46
  */
47
47
  readonly textBytes: number;
48
48
  };
49
+ /**
50
+ * The player's runtime image limits (`host/src/ui_image.rs`): a file's
51
+ * bytes and sides, the decoded pixels each plugin caches, and (SDK 1.5.19,
52
+ * player 0.5.1) the decoded pixels the nested images of one view may show.
53
+ */
54
+ export declare const IMAGE_LIMITS: Readonly<{
55
+ /** The largest image file the player reads, in bytes. */
56
+ fileBytes: number;
57
+ /** The longest side, in pixels, the player accepts for an image file. */
58
+ side: 4096;
59
+ /** The decoded pixel bytes the player caches for each plugin's images. */
60
+ pluginBytes: number;
61
+ /** The decoded pixel bytes the nested images of one view may show (player 0.5.1). */
62
+ viewBytes: number;
63
+ }>;
49
64
  /**
50
65
  * Why the player would refuse `value` (in wire form) as a property value,
51
66
  * or `null`. The same checks, order and messages as the player's
@@ -54,6 +54,21 @@ function documentText(value) {
54
54
  const doc = value.$vestraMarkdown;
55
55
  return isObject(doc) && typeof doc.text === "string" ? doc.text : null;
56
56
  }
57
+ /**
58
+ * The player's runtime image limits (`host/src/ui_image.rs`): a file's
59
+ * bytes and sides, the decoded pixels each plugin caches, and (SDK 1.5.19,
60
+ * player 0.5.1) the decoded pixels the nested images of one view may show.
61
+ */
62
+ export const IMAGE_LIMITS = Object.freeze({
63
+ /** The largest image file the player reads, in bytes. */
64
+ fileBytes: 16 * 1024 * 1024,
65
+ /** The longest side, in pixels, the player accepts for an image file. */
66
+ side: 4096,
67
+ /** The decoded pixel bytes the player caches for each plugin's images. */
68
+ pluginBytes: 64 * 1024 * 1024,
69
+ /** The decoded pixel bytes the nested images of one view may show (player 0.5.1). */
70
+ viewBytes: 32 * 1024 * 1024,
71
+ });
57
72
  /**
58
73
  * Why the player would refuse `value` (in wire form) as a property value,
59
74
  * or `null`. The same checks, order and messages as the player's
@@ -79,6 +94,9 @@ export function valueLimitProblem(value) {
79
94
  if (budget.values === L.values)
80
95
  return `UI value exceeds ${L.values} total nodes`;
81
96
  budget.values += 1;
97
+ // An image (nested from SDK 1.5.19) is one value, like a document.
98
+ if (isObject(value) && "$vestraImage" in value)
99
+ return null;
82
100
  const doc = documentText(value);
83
101
  if (doc !== null) {
84
102
  const n = bytes(doc);
package/dist/wire.d.ts CHANGED
@@ -465,11 +465,11 @@ export declare function createWire(transport: Transport): {
465
465
  };
466
466
  /** The core's own playback actions; resolves when the action finished. */
467
467
  host: {
468
- run: (action: string) => Promise<void>;
468
+ run: (action: string, seconds?: number) => Promise<void>;
469
469
  };
470
470
  step: {
471
471
  step: (unit: string, direction: string, expected?: SourceStamp) => Promise<void>;
472
- setStepSeek: (seconds: number) => Promise<void>;
472
+ setStepSeek: (seconds: number, keyframes?: boolean) => Promise<void>;
473
473
  };
474
474
  clipboard: {
475
475
  readText: () => Promise<string | null>;
@@ -570,6 +570,7 @@ export declare function createWire(transport: Transport): {
570
570
  cancel: (operationId: string) => Promise<PackageOperation>;
571
571
  list: () => Promise<PackageInfo[]>;
572
572
  setEnabled: (id: string, enabled: boolean) => Promise<void>;
573
+ reload: (id: string) => Promise<void>;
573
574
  uninstall: (id: string, removeData: boolean, expectedRevision: number, requestId: string) => Promise<PackageOperation>;
574
575
  };
575
576
  snapshot: {
package/dist/wire.js CHANGED
@@ -155,7 +155,7 @@ export function createWire(transport) {
155
155
  },
156
156
  /** The core's own playback actions; resolves when the action finished. */
157
157
  host: {
158
- run: (action) => transport.request("host.run", { action }),
158
+ run: (action, seconds) => transport.request("host.run", seconds === undefined ? { action } : { action, seconds }),
159
159
  },
160
160
  step: {
161
161
  step: (unit, direction, expected) => transport.request("player.step", {
@@ -163,7 +163,7 @@ export function createWire(transport) {
163
163
  direction,
164
164
  ...(expected ? { expected } : {}),
165
165
  }),
166
- setStepSeek: (seconds) => transport.request("player.setStepSeek", { seconds }),
166
+ setStepSeek: (seconds, keyframes) => transport.request("player.setStepSeek", keyframes === undefined ? { seconds } : { seconds, keyframes }),
167
167
  },
168
168
  clipboard: {
169
169
  readText: () => transport.request("clipboard.readText"),
@@ -284,6 +284,7 @@ export function createWire(transport) {
284
284
  cancel: (operationId) => transport.request("packages.cancel", { operationId }),
285
285
  list: () => transport.request("packages.list"),
286
286
  setEnabled: (id, enabled) => transport.request("packages.setEnabled", { id, enabled }),
287
+ reload: (id) => transport.request("packages.reload", { id }),
287
288
  uninstall: (id, removeData, expectedRevision, requestId) => transport.request("packages.uninstall", {
288
289
  id,
289
290
  removeData,
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.17: full reference
1
+ # @vestra-player/sdk 1.5.20: full reference
2
2
 
3
3
  > Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
4
4
 
@@ -583,6 +583,23 @@ Play/pause, stop, previous/next, frame steps, relative seeks, speed, volume and
583
583
 
584
584
  The difference from the `Player` methods: a `Player` control is a precise operation the plugin is responsible for (conditional, silent, its own error handling); a core action is the user's action, performed on the plugin's behalf. The action ids, menu places and default keys are in Reference: commands; how keys reach them is in Commands, menus and key bindings.
585
585
 
586
+ ##### Step seeks: exact or keyframe
587
+
588
+ ← / → (`seek-backward` / `seek-forward`) seek by the step that player.setStepSeek sets (5 s by default). By default they land on the exact time, which on a file with keyframes far apart means decoding from the previous keyframe first: a visible stall on 4K video. `setStepSeek({ seconds, keyframes: true })` (player 0.5.1, SDK 1.5.19) makes them land on a keyframe instead: forward on the first keyframe at or after position + step, backward on the last at or before position − step, and every press moves. Presses during a seek add up and continue from where it landed, and the on-screen text shows the landed time. Sources without moving video still seek exactly. The seek bar, `player.seek`, scrubs, chapters, loops and `player.step` are never affected.
589
+
590
+ For more steps of your own (Ctrl+→ and the like), call `plugin.host.run("seek-forward", { seconds: 30 })`: the same checks, mode, accumulation and text as →. In keyframe mode it resolves once the seek has landed, so `player.state.position` is current afterwards; do not show your own text for it. Set `"minPlayerVersion": "0.5.1"` when you rely on either option: older players ignore them.
591
+
592
+ ```ts
593
+ const keyframes = true; // the plugin's own setting
594
+ await plugin.player.setStepSeek({ seconds: 5, keyframes });
595
+ const seek = (delta: number) =>
596
+ plugin.host.run(delta < 0 ? "seek-backward" : "seek-forward", {
597
+ seconds: Math.abs(delta),
598
+ });
599
+ ```
600
+
601
+ In tests, `TestHost.keyframes` models the source's keyframes, and `host.coreSeeks` records each step seek with where it landed.
602
+
586
603
  ## Concept: Video and audio processing
587
604
 
588
605
  A plugin can change what the user sees and hears in three different places. They look alike in the API (each is a handle with `set`, a `status` and `onStatus`), but they act at different stages and have different owners, so choosing the right one matters:
@@ -856,7 +873,8 @@ export component ListPanel inherits Rectangle {
856
873
  ##### Names and values
857
874
 
858
875
  - Write names in camelCase or kebab-case in TypeScript: the SDK turns every capital letter into `-` plus the lower-case letter (`canMark` → `can-mark`, `strengthChanged` → `strength-changed`). Fields inside struct values are **not** converted, so spell struct fields exactly as the Slint struct does (and without `_`, which Slint treats as `-`).
859
- - Values must be JSON: booleans, numbers, strings, arrays (Slint models) and objects (Slint structs). Two exceptions: an image from ui.image, only as the **whole** value of an `image` property (nested in an array or object it is `invalid`), and a document from ui.markdown wherever the Slint type is `[MarkdownBlock]`: the whole value, or (SDK 1.5.4, the player after 0.4.0) a field at any depth, such as the `body` of each message in a chat list.
876
+ - Values must be JSON: booleans, numbers, strings, arrays (Slint models) and objects (Slint structs). Two exceptions: an image from ui.image or ui.remoteImage wherever the Slint type is `image`: the whole value, or (SDK 1.5.19, player 0.5.1) a field at any depth, such as the logo of each row of a list; and a document from ui.markdown wherever the Slint type is `[MarkdownBlock]`: the whole value, or (SDK 1.5.4, the player after 0.4.0) a field at any depth, such as the `body` of each message in a chat list.
877
+ - Images in rows: give small pictures `maxSide` (about twice the drawn size, `64` for a 32 px logo), so that the player decodes them small. The images nested in one view's values may show at most 32 MB of decoded pixels together; a write past that is refused with `limit`. A nested image whose file fails the player's checks is drawn empty, and the rest of the row is shown. Rows with images update in place, so a `ListView` keeps its scroll position when the list is written again.
860
878
  - Every name must exist in the Slint file. Writing a property the file does not declare makes a tool window's `show()` or a panel's `ready` fail, and in a settings page it keeps the Options window from opening; for a tool window, so does a callback bound with `on()` that the file does not declare. Once the view is open, such writes are reported to onError.
861
879
 
862
880
  ##### set, bind and the update pass
@@ -1850,7 +1868,7 @@ The player no longer keeps a previous version of a plugin. An upgrade replaces t
1850
1868
 
1851
1869
  - There is no `packages.rollback`: the player answers the wire method with `unsupported` ("unknown package method"), and SDK 1.5.17 no longer has it.
1852
1870
  - `packages.list()` and `packages.onChange` entries have no `previousVersion` (always `null` with SDK 1.5.16, removed in 1.5.17).
1853
- - Built-in plugins (`builtin: true`) run in place from the player installation. They can be disabled and enabled, never uninstalled or replaced; `uninstall` fails with `unsupported`. A built-in's `directory` is inside the installation (`<install resources>/plugins/<id>`) and read-only.
1871
+ - Built-in plugins (`builtin: true`) run in place from the player installation. Users manage only third-party plugins: a built-in can be neither uninstalled, replaced nor (from SDK 1.5.18 and the players released with it) disabled; `uninstall` and `setEnabled(id, false)` fail with `unsupported`, and a built-in an older player let the user disable runs again after the upgrade. A built-in's `directory` is inside the installation (`<install resources>/plugins/<id>`) and read-only.
1854
1872
  - Third-party plugins can still be uninstalled.
1855
1873
 
1856
1874
  Why: built-ins now run from the read-only installation instead of a copy, and an upgrade replaces the package instead of keeping the old one beside it. Going back to an older version becomes an ordinary uninstall and install that the user starts.
@@ -1862,14 +1880,15 @@ Why: built-ins now run from the read-only installation instead of a copy, and an
1862
1880
  Before:
1863
1881
 
1864
1882
  ```ts nocheck
1865
- if (info.previousVersion !== null) showButton("Roll back", () => plugin.packages.rollback(info.id));
1883
+ if (info.previousVersion !== null)
1884
+ showButton("Roll back", () => plugin.packages.rollback(info.id));
1866
1885
  ```
1867
1886
 
1868
1887
  After: delete both. There is no replacement call; nothing on the player can restore an earlier package. Remove the button, menu item or command, and any state, text or test that depends on `previousVersion`. With SDK 1.5.17 the compiler finds every remaining use.
1869
1888
 
1870
1889
  #### 2. Going back to an older version
1871
1890
 
1872
- Managing plugins has exactly three operations: **install**, **uninstall** and **disable/enable**. There is no rollback, no "install older version" and no "Undo install".
1891
+ Managing third-party plugins has exactly three operations: **install**, **uninstall** and **disable/enable**. Built-ins have none of them. There is no rollback, no "install older version" and no "Undo install".
1873
1892
 
1874
1893
  To go back to an older version, the user uninstalls the plugin and installs the older version's `.vxp` file themselves, as two ordinary operations. Do not offer an action that does both, an older-version install from a store, or an "Undo" after an install (the official store and the AI window drop theirs). Uninstalling without `removeData` keeps the plugin's data, and a reinstall from the same source reconnects to it; a `.vxp` file is a different source from a repository, so a plugin first installed from the store gets a new, empty data directory.
1875
1894
 
@@ -1884,13 +1903,13 @@ Check `builtin` before offering actions:
1884
1903
  ```ts
1885
1904
  for (const info of await plugin.packages.list()) {
1886
1905
  const actions = info.builtin
1887
- ? [info.state === "disabled" ? "Enable" : "Disable"]
1888
- : ["Disable", "Uninstall"];
1906
+ ? []
1907
+ : [info.state === "disabled" ? "Enable" : "Disable", "Uninstall"];
1889
1908
  console.log(info.id, actions);
1890
1909
  }
1891
1910
  ```
1892
1911
 
1893
- - Offer only disable and enable for a built-in (`packages.setEnabled`); never uninstall or install over it.
1912
+ - Offer no action for a built-in: never disable, uninstall or install over it (the player answers `unsupported`). Built-ins that an older player listed as `disabled` come back as running.
1894
1913
  - Never write into a built-in's `directory`, or into any plugin's `directory`: read `manifest.json` from it if you need to. A plugin writes only under its own plugin.dataDirectory.
1895
1914
 
1896
1915
  #### 5. Tests
@@ -1899,17 +1918,18 @@ With SDK 1.5.17, `host.packages` entries have no `previousVersion` field: delete
1899
1918
 
1900
1919
  #### Timeline
1901
1920
 
1902
- | SDK | Player | What happens |
1903
- | --- | --- | --- |
1904
- | 1.5.16 | ships with it | `Packages.rollback` and `PluginInfo.previousVersion` (and the wire field) are `@deprecated`. The player and `TestHost` answer rollback with `unsupported` and list `previousVersion: null`. Code still compiles. |
1921
+ | SDK | Player | What happens |
1922
+ | ------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1923
+ | 1.5.16 | ships with it | `Packages.rollback` and `PluginInfo.previousVersion` (and the wire field) are `@deprecated`. The player and `TestHost` answer rollback with `unsupported` and list `previousVersion: null`. Code still compiles. |
1905
1924
  | 1.5.17 | ships with it | **Removed:** `Packages.rollback`, `PluginInfo.previousVersion`, the wire method `packages.rollback` and the wire field `previousVersion`, `TestHost`'s `previousVersion` field and its `packages.rollback` handler, and `Packages.rollback` in the `plugins.manage` capability. Remaining uses are TypeScript compile errors. |
1925
+ | 1.5.18 | ships with it | Built-ins can no longer be disabled: the player and `TestHost` answer `setEnabled(builtin, false)` with `unsupported` (user ruling 2026-10-26). |
1906
1926
 
1907
1927
  `apiVersion` stays `vestra/1` in both releases, as when 1.5.8 removed `PackageCandidate.trial()`.
1908
1928
 
1909
1929
  #### Before you hand it in
1910
1930
 
1911
1931
  - [ ] No call to `packages.rollback`; no UI or logic gated on `previousVersion`.
1912
- - [ ] Built-ins offer only disable and enable.
1932
+ - [ ] Built-ins offer no Disable, Enable or Uninstall.
1913
1933
  - [ ] Nothing writes into a plugin's `directory`.
1914
1934
  - [ ] No Undo after an install, no older-version install; older versions go through uninstall and a user-chosen `.vxp`.
1915
1935
  - [ ] Tests compile and pass with SDK 1.5.17; no `previousVersion` is left in `host.packages` test data.
@@ -2155,7 +2175,7 @@ The table lists the API whose documentation names a player version, newest last:
2155
2175
  | 0.4.1 | PluginError, FileResolverOptions, PackageCandidate.install, PluginWarning.code, Packages.validate |
2156
2176
  | 0.4.2 | Resolvers.fileTypes, Resolvers.onFileTypesChange |
2157
2177
  | 0.4.3 | CommandOptions.path, PropValue, View.set, Appearance.setLanguage, TemplateArgs, Text, ConfirmOptions.args, OsdPanel.set, OsdPanelOptions.maxColumns, Osd.show |
2158
- | 0.5.1 | SlotState.reason, Panel.setInteraction, PlaybackStats.presentedFps |
2178
+ | 0.5.1 | ImageRef, ImageOptions.maxSide, PropValue, Ui.image, Ui.remoteImage, SlotState.reason, Panel.setInteraction, PlaybackStats.presentedFps, Player.setStepSeek, Host.run, Packages.reload |
2159
2179
 
2160
2180
  The Slint library carries no version tags: `Theme.danger` needs player 0.3.0; `@vestra/icons.slint` and the palette `vestra-light` need 0.4.0 (Theme).
2161
2181
 
@@ -2176,7 +2196,7 @@ A plugin lists what it may do in the manifest's `capabilities` (Manifest). A cal
2176
2196
  | `ui.window` | Windows.create | — | Open tool windows | |
2177
2197
  | `ui.mount` | Panels.mount, Panels.bottom, Panels.sidebar, Panels.backdrop, PanelSlot.onChange, Panel.onChange, BackdropSlot.onChange | — | Show panels in the bottom bar, the sidebar or behind the video | |
2178
2198
  | `ui.dialog` | Dialogs.confirm, Dialogs.openFile, Dialogs.openFiles, Dialogs.openDirectory, Dialogs.saveFile | — | Show file pickers and confirmations | |
2179
- | `plugins.manage` | Packages.validate, Packages.validateArchive, Packages.setEnabled, Packages.uninstall | — | Install, uninstall and disable other plugins | yes |
2199
+ | `plugins.manage` | Packages.validate, Packages.validateArchive, Packages.setEnabled, Packages.reload, Packages.uninstall | — | Install, uninstall and disable other plugins | yes |
2180
2200
  | `processing.video` | Plugin.video.filter, Plugin.video.view, Plugin.video.render | `playback.read` | Process the picture (filters, shaders) | |
2181
2201
  | `processing.audio` | Plugin.audio.filter | `playback.read`, `events` | Process the sound | |
2182
2202
  | `subtitles.read` | Subtitles.state, Subtitles.onChange, Subtitles.refresh | — | Read subtitles | |
@@ -2649,7 +2669,6 @@ Every command the player (`host`) and its built-in plugins register when the pla
2649
2669
  | Settings ▸ Keyboard Shortcuts… | | `vestra.hotkeys/keys` |
2650
2670
  | Settings ▸ Mouse… | | `vestra.hotkeys/mouse` |
2651
2671
  | Settings ▸ Ignore Default Shortcuts | | `vestra.hotkeys/ignore-defaults` |
2652
- | Settings ▸ Plugin Manager… | | `vestra.plugins/manage` |
2653
2672
  | Settings ▸ Appearance ▸ Vestra Dark | | `vestra.preferences/theme.vestra` |
2654
2673
  | Settings ▸ Appearance ▸ Vestra Light | | `vestra.preferences/theme.vestra-light` |
2655
2674
  | Settings ▸ Appearance ▸ Show Logo When Idle | | `vestra.visualizer/idle-logo` |
@@ -3535,6 +3554,20 @@ interface ImageRef {
3535
3554
  }
3536
3555
  ```
3537
3556
 
3557
+ ### ImageOptions
3558
+
3559
+ Options of Ui.image and Ui.remoteImage.
3560
+
3561
+ ```ts
3562
+ interface ImageOptions {
3563
+ /**
3564
+ * Decode the image scaled down, aspect kept, so that neither side is over this many pixels (an integer 1–4096); smaller images stay as they are. Use about twice the drawn size (64 for a 32 px logo) so that it stays sharp on high-density screens. It keeps the decoded memory small: a 1024×1024 icon is 4 MB decoded, 16 KB at `maxSide: 64`.
3565
+ * @player 0.5.1; older players refuse the reference (`invalid`).
3566
+ */
3567
+ maxSide?: number;
3568
+ }
3569
+ ```
3570
+
3538
3571
  ### MarkdownRef
3539
3572
 
3540
3573
  A Markdown document for `MarkdownView` or `MarkdownBlocks`, made by
@@ -3569,9 +3602,8 @@ interface MarkdownOptions {
3569
3602
 
3570
3603
  ### PropValue
3571
3604
 
3572
- The value of one Slint property: JSON with Markdown documents at any
3573
- depth (PropData), or an image as the whole value of an `image`
3574
- property.
3605
+ The value of one Slint property: JSON with Markdown documents and images
3606
+ at any depth (PropData).
3575
3607
 
3576
3608
  ```ts
3577
3609
  type PropValue = PropData | ImageRef;
@@ -3579,11 +3611,11 @@ type PropValue = PropData | ImageRef;
3579
3611
 
3580
3612
  ### PropData
3581
3613
 
3582
- JSON whose values may also be Markdown documents, at any depth: the part
3583
- of PropValue that nests.
3614
+ JSON whose values may also be Markdown documents and (SDK 1.5.19) images,
3615
+ at any depth: the part of PropValue that nests.
3584
3616
 
3585
3617
  ```ts
3586
- type PropData = null | boolean | number | string | MarkdownRef | Text | TextList | PropData[] | {
3618
+ type PropData = null | boolean | number | string | MarkdownRef | ImageRef | Text | TextList | PropData[] | {
3587
3619
  [key: string]: PropData;
3588
3620
  };
3589
3621
  ```
@@ -3609,10 +3641,22 @@ interface Ui {
3609
3641
  * Reference a PNG or JPEG file for an `image` property.
3610
3642
  * @capability None.
3611
3643
  * @param path - Absolute path of the image file, inside the package directory or the data directory.
3644
+ * @param options - `maxSide` (SDK 1.5.19, player 0.5.1): decode the image scaled down, aspect kept, so that neither side is over it.
3612
3645
  * @returns A new ImageRef, owned by this scope.
3613
- * @throws invalid (throws) `path` is not a non-empty string.
3646
+ * @throws invalid (throws) `path` is not a non-empty string, or `maxSide` is not an integer 1–4096.
3614
3647
  */
3615
- image(path: string): ImageRef;
3648
+ image(path: string, options?: ImageOptions): ImageRef;
3649
+ /**
3650
+ * Download an `https:` PNG or JPEG image (a thumbnail, a repository's plugin icon) and reference the local copy, as Ui.image does.
3651
+ * @capability None (the plugin process has network access).
3652
+ * @player Any (the reference is an ordinary ImageRef); nested in a value, player 0.5.1.
3653
+ * @param url - The image's `https:` address.
3654
+ * @param options - As for Ui.image.
3655
+ * @returns The reference, owned by this scope, once the file is in the cache.
3656
+ * @throws invalid (rejects) `url` is not an `https:` address, or `maxSide` is not an integer 1–4096.
3657
+ * @throws unavailable (rejects) the download failed, timed out, was too large, or is not a PNG or JPEG image; or the plugin has no data directory.
3658
+ */
3659
+ remoteImage(url: string, options?: ImageOptions): Promise<ImageRef>;
3616
3660
  /**
3617
3661
  * Prepare a Markdown document for `MarkdownView` (`@vestra/markdown.slint`).
3618
3662
  * @capability None.
@@ -5179,10 +5223,28 @@ interface Player {
5179
5223
  */
5180
5224
  step(unit: "frame" | "keyframe", direction: "backward" | "forward", options?: Conditional): Promise<void>;
5181
5225
  /**
5182
- * The core's first-tier relative seek (`seek-backward` / `seek-forward`, ← / →): 0.1–3600 seconds in tenths, default 5. The newest call of any plugin wins; it reverts when that plugin stops.
5226
+ * The core's first-tier relative seek (`seek-backward` / `seek-forward`,
5227
+ * ← / →): 0.1–3600 seconds in tenths, default 5. The newest call of any
5228
+ * plugin wins; it reverts when that plugin stops.
5229
+ *
5230
+ * `keyframes: true` (player 0.5.1, SDK 1.5.19): the step seeks
5231
+ * (`seek-backward` / `seek-forward`, including Host.run with
5232
+ * `seconds` and the picture-in-picture skips) land on a keyframe instead
5233
+ * of the exact time: forward on the first keyframe at or after position +
5234
+ * step, backward on the last at or before position − step, and they
5235
+ * always move. Presses during a seek add up and continue from where it
5236
+ * landed; the on-screen text shows the landed time. Sources without moving
5237
+ * video seek exactly. The seek bar, Player.seek, scrubs, chapters,
5238
+ * loops and Player.step are unaffected. Omitted or `false`: exact
5239
+ * seeks, as before. `seconds` and `keyframes` are written as a pair: the
5240
+ * newest call sets both.
5241
+ *
5242
+ * Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
5243
+ * the manifest when you rely on it.
5183
5244
  */
5184
5245
  setStepSeek(options: {
5185
5246
  seconds: number;
5247
+ keyframes?: boolean;
5186
5248
  }): Promise<void>;
5187
5249
  /** The chapters of the current media (empty without media or chapters), read on demand; `sourceId` tells which source they belong to. `playback.read`. */
5188
5250
  chapters(): Promise<{
@@ -5224,7 +5286,7 @@ The core's basic playback actions (menu commands owned by the player).
5224
5286
  | `play-pause` | Space | pause / resume, replay after the end |
5225
5287
  | `previous` / `next` | PageUp, Mouse:X1 / PageDown, Mouse:X2 | ask the sequence provider |
5226
5288
  | `frame-back` / `frame-forward` | D / F | `player.step("frame", …)` |
5227
- | `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds |
5289
+ | `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds (or `host.run`'s `seconds`), exact or to keyframes as `setStepSeek` says |
5228
5290
  | `rate-normal`, `rate-down`, `rate-up` | Z, X, C | 1×, ±0.1 within 0.25–4 |
5229
5291
  | `volume-up` / `volume-down` | ↑ / ↓, wheel | ±5 (`volume-up` stops at 100) |
5230
5292
  | `mute` | M | toggle mute |
@@ -5250,6 +5312,23 @@ interface Host {
5250
5312
  * Prefer this over reimplementing playback buttons.
5251
5313
  */
5252
5314
  run(action: HostAction): Promise<void>;
5315
+ /**
5316
+ * One step seek of `seconds` (0.1–3600 in tenths) instead of the
5317
+ * Player.setStepSeek step: the same checks, mode (exact or
5318
+ * keyframes), accumulation and on-screen text as ← / →. Use it for more
5319
+ * seek steps of your own (Ctrl+→ and the like) so that they add up with
5320
+ * the core's and land the same way.
5321
+ *
5322
+ * In keyframe mode it resolves once the seek has landed (so
5323
+ * `player.state.position` is current after the `await`); presses that add
5324
+ * up resolve together, the last with the text. In exact mode it resolves
5325
+ * once the seek is submitted, as before. `invalid` for `seconds` out of
5326
+ * range.
5327
+ * @player 0.5.1 (set `"minPlayerVersion": "0.5.1"`: older players ignore `seconds` and seek by the `setStepSeek` step)
5328
+ */
5329
+ run(action: "seek-backward" | "seek-forward", options: {
5330
+ seconds: number;
5331
+ }): Promise<void>;
5253
5332
  }
5254
5333
  ```
5255
5334
 
@@ -6007,7 +6086,9 @@ interface PluginInfo {
6007
6086
  * The package directory (read its `manifest.json` for display fields). A built-in's directory is inside the player installation (`<install resources>/plugins/<id>`) and read-only: a plugin writes only under its own Plugin.dataDirectory.
6008
6087
  */
6009
6088
  directory: string;
6010
- /** Shipped with the player: it can be disabled, not uninstalled or replaced. `false` from players before 0.2.0. */
6089
+ /**
6090
+ * Shipped with the player: it always runs; it cannot be disabled, uninstalled or replaced (older players, before SDK 1.5.18, let it be disabled). `false` from players before 0.2.0.
6091
+ */
6011
6092
  builtin: boolean;
6012
6093
  /** Where an installed plugin came from; `null` for development loads. */
6013
6094
  source: PackageSource | null;
@@ -6085,9 +6166,26 @@ interface Packages {
6085
6166
  /** The new list after any start, stop, failure, install or uninstall. No capability. */
6086
6167
  onChange(listener: (plugins: PluginInfo[]) => void): Disposable;
6087
6168
  /**
6088
- * Stop or start a plugin (`plugins.manage`). An installed plugin stays disabled across launches; a development load only for this run. `invalid` to disable oneself, `missing` unknown. `busy` while it is being installed.
6169
+ * Stop or start a plugin (`plugins.manage`). An installed plugin stays disabled across launches; a development load only for this run. `invalid` to disable oneself, `missing` unknown. `busy` while it is being installed. `unsupported` to disable a built-in (PluginInfo.builtin): users manage third-party plugins only (players released with SDK 1.5.18 on; older ones disabled built-ins too).
6089
6170
  */
6090
6171
  setEnabled(id: string, enabled: boolean): Promise<void>;
6172
+ /**
6173
+ * Restart a plugin (`plugins.manage`): stop it and start it again from the
6174
+ * same package with the same data directory, for example to recover one
6175
+ * that misbehaves. Built-ins included. A stopped or failed plugin is just
6176
+ * started. The disabled flag is never written, so a reload never leaves
6177
+ * a plugin disabled.
6178
+ *
6179
+ * Resolves once the plugin is stopped and its start is scheduled; the
6180
+ * player starts it as soon as the old process is gone (at most 20 s).
6181
+ * Follow it with Packages.onChange: `starting`, then `running`,
6182
+ * or `failed` with the reason.
6183
+ *
6184
+ * `invalid` for oneself or a disabled plugin (enable it instead),
6185
+ * `missing` unknown, `busy` while it is being installed or reloaded.
6186
+ * @player 0.5.1; older players reject it as an unknown request (`unsupported`).
6187
+ */
6188
+ reload(id: string): Promise<void>;
6091
6189
  /**
6092
6190
  * Remove an installed plugin (`plugins.manage`); `removeData` also deletes its data directory, otherwise reinstalling it from the same source reconnects to its data. Its user bindings stay (as orphans). `unsupported` for built-ins and development loads, `missing` unknown, `invalid` for oneself.
6093
6191
  */
@@ -6987,12 +7085,16 @@ class TestHost implements Transport {
6987
7085
  kind: string;
6988
7086
  title?: string | null;
6989
7087
  }[]): void;
6990
- /** Runtime images written to views (`plugin.ui.image`), newest last. The SDK refuses images of released scopes before anything is sent. */
7088
+ /**
7089
+ * Runtime images written to views (`plugin.ui.image`, `plugin.ui.remoteImage`), whole values and (SDK 1.5.19) images nested in rows, newest last. The SDK refuses images of released scopes before anything is sent. The test host does not read the files, so it does not check the player's per-view budget (`IMAGE_LIMITS.viewBytes`).
7090
+ */
6991
7091
  readonly images: {
6992
7092
  /** The image file's path, as given to `ui.image`. */
6993
7093
  path: string;
6994
7094
  /** The SDK's reference number for that `ImageRef`. */
6995
7095
  ref: number;
7096
+ /** Its `maxSide` option, when given. */
7097
+ maxSide?: number;
6996
7098
  }[];
6997
7099
  /**
6998
7100
  * The file an image property shows, or `null` when the value is not a runtime image. View properties hold images in their wire form; this reads the path back.
@@ -7181,7 +7283,7 @@ class TestHost implements Transport {
7181
7283
  /** Export jobs by job id. Each state query advances a job one step (`queued` → `running` → `completed`; `cancelling` → `cancelled`). Wire-level. */
7182
7284
  readonly jobs: Map<number, MediaJob>;
7183
7285
  /**
7184
- * `openStream`'s transport: a Unix socket (macOS, Linux) or, with `"tcp"`, loopback TCP where the plugin must send the token first (Windows). Set it before the plugin calls `openStream`.
7286
+ * `openStream`'s transport: a local stream (a Unix socket on macOS and Linux, a named pipe on Windows) or, with `"tcp"`, loopback TCP where the plugin must send the token first, as the player on Windows answers. Set it before the plugin calls `openStream`.
7185
7287
  */
7186
7288
  producerTransport: "unix" | "tcp";
7187
7289
  /** Generated streams (`player.openStream`) by producer id: the fake player end of each frame connection. Wire-level beyond the FakeProducer fields. */
@@ -7313,6 +7415,25 @@ class TestHost implements Transport {
7313
7415
  constructor();
7314
7416
  /** The `player.setStepSeek` value in effect (default 5). */
7315
7417
  stepSeek: number;
7418
+ /** The `player.setStepSeek` `keyframes` in effect (default `false`, SDK 1.5.19). */
7419
+ stepSeekKeyframes: boolean;
7420
+ /**
7421
+ * Test data: the keyframe times (seconds, ascending) of the current source, for keyframe step seeks. `null` (the default): none modelled, and step seeks are exact even with `stepSeekKeyframes`.
7422
+ */
7423
+ keyframes: number[] | null;
7424
+ /**
7425
+ * Every step seek the core ran (`seek-backward` / `seek-forward`, from a key, the menu or `host.run`), newest last: the step, whether it went to a keyframe, and where it landed.
7426
+ */
7427
+ readonly coreSeeks: {
7428
+ /** `seek-backward` or `seek-forward`. */
7429
+ action: string;
7430
+ /** The step: `host.run`'s `seconds`, else `setStepSeek`'s. */
7431
+ seconds: number;
7432
+ /** It landed on a keyframe (`stepSeekKeyframes` and keyframes modelled). */
7433
+ keyframes: boolean;
7434
+ /** The new position. */
7435
+ landed: number;
7436
+ }[];
7316
7437
  /** Every `player.step`, newest last. */
7317
7438
  readonly steps: {
7318
7439
  /** `"frame"` or `"keyframe"`. */
@@ -7426,7 +7547,7 @@ class TestHost implements Transport {
7426
7547
  /** Every `packages.setEnabled` / `uninstall` / `install`, newest last. */
7427
7548
  readonly packageCalls: {
7428
7549
  /** Which call. */
7429
- method: "setEnabled" | "uninstall" | "install";
7550
+ method: "setEnabled" | "reload" | "uninstall" | "install";
7430
7551
  /** The target plugin id. */
7431
7552
  id: string;
7432
7553
  /** `setEnabled`'s value. */
@@ -7443,7 +7564,7 @@ class TestHost implements Transport {
7443
7564
  */
7444
7565
  readonly candidates: Map<string, PackageManifest>;
7445
7566
  /**
7446
- * Remote Markdown images (`plugin.ui.markdown`): address → a local PNG or JPEG file served for it, or `null` for a failing download. Nothing is fetched from the network in tests; unknown addresses fail.
7567
+ * Remote images (`plugin.ui.markdown`, `plugin.ui.remoteImage`): address → a local PNG or JPEG file served for it, or `null` for a failing download. Nothing is fetched from the network in tests; unknown addresses fail.
7447
7568
  */
7448
7569
  readonly remoteImages: Map<string, string | null>;
7449
7570
  /**
@@ -7872,6 +7993,25 @@ const UI_VALUE_LIMITS: {
7872
7993
  };
7873
7994
  ```
7874
7995
 
7996
+ ### IMAGE_LIMITS
7997
+
7998
+ The player's runtime image limits (`host/src/ui_image.rs`): a file's
7999
+ bytes and sides, the decoded pixels each plugin caches, and (SDK 1.5.19,
8000
+ player 0.5.1) the decoded pixels the nested images of one view may show.
8001
+
8002
+ ```ts
8003
+ const IMAGE_LIMITS: Readonly<{
8004
+ /** The largest image file the player reads, in bytes. */
8005
+ fileBytes: number;
8006
+ /** The longest side, in pixels, the player accepts for an image file. */
8007
+ side: 4096;
8008
+ /** The decoded pixel bytes the player caches for each plugin's images. */
8009
+ pluginBytes: number;
8010
+ /** The decoded pixel bytes the nested images of one view may show (player 0.5.1). */
8011
+ viewBytes: number;
8012
+ }>;
8013
+ ```
8014
+
7875
8015
  ### HostError
7876
8016
 
7877
8017
  A definite refusal from the player (or the plugin runner), with a stable