@vestra-player/sdk 1.5.18 → 1.5.21

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.
@@ -578,11 +578,12 @@ export interface Commands {
578
578
  * An image file for a Slint `image` property, made by {@link Ui.image}.
579
579
  *
580
580
  * @remarks
581
- * Opaque: pass it as the **whole** value of an `image` property with
582
- * {@link View.set} or {@link View.bind}; it cannot sit inside a struct or an
583
- * array. Making the reference does not read the file: the player checks and
584
- * decodes it each time a view uses it ({@link Ui.image} has the rules and
585
- * limits).
581
+ * Opaque: pass it where the Slint type is `image`, with {@link View.set} or
582
+ * {@link View.bind}: the whole value of an `image` property or, from SDK
583
+ * 1.5.19 (player 0.5.1), an `image` field at any depth, such as the logo of
584
+ * each row of a list. Making the reference does not read the file: the
585
+ * player checks and decodes it each time a view uses it ({@link Ui.image}
586
+ * has the rules and limits).
586
587
  *
587
588
  * A reference belongs to the {@link Scope} that created it. Once that scope
588
589
  * is disposed, writing the reference fails with `invalid` (reported to
@@ -596,6 +597,22 @@ export interface Commands {
596
597
  * await cover.show();
597
598
  * ```
598
599
  *
600
+ * @example A logo in each row
601
+ * ```slint
602
+ * export struct Row { key: string, name: string, icon: image }
603
+ * ```
604
+ * ```ts
605
+ * const items = [{ id: "a", name: "A", iconPath: `${plugin.dataDirectory}/a.png` }];
606
+ * const list = plugin.windows.create("ui/list.slint");
607
+ * list.bind(() => ({
608
+ * rows: items.map((item) => ({
609
+ * key: item.id,
610
+ * name: item.name,
611
+ * icon: plugin.ui.image(item.iconPath, { maxSide: 64 }),
612
+ * })),
613
+ * }));
614
+ * ```
615
+ *
599
616
  * @see {@link Ui.image}, [Windows, panels, settings pages, dialogs and the OSD](../../docs/concepts/ui.md)
600
617
  * @since 1.0.0
601
618
  * @category UI
@@ -606,6 +623,24 @@ export interface ImageRef {
606
623
  /** Brand that marks the value as an image reference; never construct one yourself. */
607
624
  readonly __vestraImage: true;
608
625
  }
626
+ /**
627
+ * Options of {@link Ui.image} and {@link Ui.remoteImage}.
628
+ *
629
+ * @since 1.5.19
630
+ * @category UI
631
+ */
632
+ export interface ImageOptions {
633
+ /**
634
+ * Decode the image scaled down, aspect kept, so that neither side is over
635
+ * this many pixels (an integer 1–4096); smaller images stay as they are.
636
+ * Use about twice the drawn size (64 for a 32 px logo) so that it stays
637
+ * sharp on high-density screens. It keeps the decoded memory small: a
638
+ * 1024×1024 icon is 4 MB decoded, 16 KB at `maxSide: 64`.
639
+ *
640
+ * @player 0.5.1; older players refuse the reference (`invalid`).
641
+ */
642
+ maxSide?: number;
643
+ }
609
644
  /**
610
645
  * A Markdown document for `MarkdownView` or `MarkdownBlocks`, made by
611
646
  * {@link Ui.markdown}.
@@ -665,9 +700,8 @@ export interface MarkdownOptions {
665
700
  remoteImages?: boolean;
666
701
  }
667
702
  /**
668
- * The value of one Slint property: JSON with Markdown documents at any
669
- * depth ({@link PropData}), or an image as the whole value of an `image`
670
- * property.
703
+ * The value of one Slint property: JSON with Markdown documents and images
704
+ * at any depth ({@link PropData}).
671
705
  *
672
706
  * @remarks
673
707
  * JSON becomes Slint values as you would expect: booleans for `bool`,
@@ -684,13 +718,20 @@ export interface MarkdownOptions {
684
718
  *
685
719
  * A {@link MarkdownRef} goes where the Slint type is `[MarkdownBlock]`: the
686
720
  * whole value, or a field at any depth (SDK 1.5.4; older players refuse a
687
- * nested one). An {@link ImageRef} only as the whole value: nested in an
688
- * array or object it is `invalid`.
721
+ * nested one). An {@link ImageRef} goes where the Slint type is `image`: the
722
+ * whole value, or a field at any depth (SDK 1.5.19, player 0.5.1; older
723
+ * players refuse a nested one with `invalid`). The images nested in the
724
+ * values of one view (a window, panel or settings page) may show at most
725
+ * 32 MB of decoded pixels together (`IMAGE_LIMITS.viewBytes` in
726
+ * `@vestra-player/sdk/runtime`): a write
727
+ * that would pass it is refused with `limit`; use
728
+ * {@link ImageOptions.maxSide} for small pictures such as logos and
729
+ * thumbnails.
689
730
  *
690
731
  * Limits per value (`UI_VALUE_LIMITS` in `@vestra-player/sdk/testing`,
691
732
  * player 0.4.3; older players: 4096 values and 1024 rows): nesting depth 8,
692
733
  * 16384 values in total (every array, object, string, number, boolean and
693
- * null counts, the value itself included; a document is one), 4096 rows
734
+ * null counts, the value itself included; a document or an image is one), 4096 rows
694
735
  * per array, 64 KiB per string or field name and 1 MiB of text in total,
695
736
  * field names and the text of its Markdown documents included (256 KB per
696
737
  * document). A list of rows with 8 fields is 9 values per row, so about
@@ -708,8 +749,8 @@ export interface MarkdownOptions {
708
749
  */
709
750
  export type PropValue = PropData | ImageRef;
710
751
  /**
711
- * JSON whose values may also be Markdown documents, at any depth: the part
712
- * of {@link PropValue} that nests.
752
+ * JSON whose values may also be Markdown documents and (SDK 1.5.19) images,
753
+ * at any depth: the part of {@link PropValue} that nests.
713
754
  *
714
755
  * @example A message list whose rows carry a document
715
756
  * ```ts
@@ -721,7 +762,7 @@ export type PropValue = PropData | ImageRef;
721
762
  * @since 1.5.4
722
763
  * @category UI
723
764
  */
724
- export type PropData = null | boolean | number | string | MarkdownRef | Text | TextList | PropData[] | {
765
+ export type PropData = null | boolean | number | string | MarkdownRef | ImageRef | Text | TextList | PropData[] | {
725
766
  [key: string]: PropData;
726
767
  };
727
768
  /**
@@ -778,10 +819,13 @@ export interface Ui {
778
819
  * the main window being rebuilt): it must be inside this plugin's package
779
820
  * directory or {@link Plugin.dataDirectory} after following links, at most
780
821
  * 16 MB and 4096×4096 pixels, and PNG or JPEG by content (the extension
781
- * does not matter). A failed check is a failed property write: `missing`
782
- * for a file that does not exist, `invalid` otherwise. It goes to
783
- * {@link Scope.onError} and the property keeps its previous image; in the
784
- * initial properties of a tool window it makes {@link Window.show} reject.
822
+ * does not matter). A failed check of a whole-value image is a failed
823
+ * property write: `missing` for a file that does not exist, `invalid`
824
+ * otherwise. It goes to {@link Scope.onError} and the property keeps its
825
+ * previous image; in the initial properties of a tool window it makes
826
+ * {@link Window.show} reject. A nested image (a field of a row, SDK
827
+ * 1.5.19) that fails its check is drawn empty instead, and the rest of
828
+ * the value is shown, as a Markdown image shows its alt text.
785
829
  *
786
830
  * The player caches decoded images per plugin (up to 64 MB, least recently
787
831
  * used first) and does not watch files. Every call returns a new value, so
@@ -792,9 +836,12 @@ export interface Ui {
792
836
  *
793
837
  * @param path - Absolute path of the image file, inside the package
794
838
  * directory or the data directory.
839
+ * @param options - `maxSide` (SDK 1.5.19, player 0.5.1): decode the image
840
+ * scaled down, aspect kept, so that neither side is over it.
795
841
  * @returns A new {@link ImageRef}, owned by this scope.
796
842
  *
797
- * @throws `invalid` (throws) `path` is not a non-empty string.
843
+ * @throws `invalid` (throws) `path` is not a non-empty string, or
844
+ * `maxSide` is not an integer 1–4096.
798
845
  *
799
846
  * @example Show a thumbnail the plugin rendered
800
847
  * ```ts
@@ -807,7 +854,51 @@ export interface Ui {
807
854
  *
808
855
  * @since 1.0.0
809
856
  */
810
- image(path: string): ImageRef;
857
+ image(path: string, options?: ImageOptions): ImageRef;
858
+ /**
859
+ * Download an `https:` PNG or JPEG image (a thumbnail, a repository's
860
+ * plugin icon) and reference the local copy, as {@link Ui.image} does.
861
+ *
862
+ * @remarks
863
+ * Downloads like {@link Ui.markdown}'s remote images: Node's `fetch` in
864
+ * this plugin's process, at most 4 MB and 10 s per image, 4 downloads at a
865
+ * time per plugin, into `<dataDirectory>/.vestra-cache/images/`; the cache
866
+ * keeps 32 MB and fetches an address again after 24 hours, so a list that
867
+ * shows the same thumbnails again reads them from disk.
868
+ *
869
+ * @capability None (the plugin process has network access).
870
+ * @player Any (the reference is an ordinary {@link ImageRef}); nested in a
871
+ * value, player 0.5.1.
872
+ *
873
+ * @param url - The image's `https:` address.
874
+ * @param options - As for {@link Ui.image}.
875
+ * @returns The reference, owned by this scope, once the file is in the
876
+ * cache.
877
+ *
878
+ * @throws `invalid` (rejects) `url` is not an `https:` address, or
879
+ * `maxSide` is not an integer 1–4096.
880
+ * @throws `unavailable` (rejects) the download failed, timed out, was too
881
+ * large, or is not a PNG or JPEG image; or the plugin has no data
882
+ * directory.
883
+ *
884
+ * @example Search results with thumbnails
885
+ * ```ts
886
+ * const results = [{ title: "A", thumbnailUrl: "https://example.com/a.jpg" }];
887
+ * const placeholder = plugin.ui.image(`${plugin.dataDirectory}/blank.png`);
888
+ * const rows = await Promise.all(
889
+ * results.map(async (r) => ({
890
+ * title: r.title,
891
+ * thumb: await plugin.ui
892
+ * .remoteImage(r.thumbnailUrl, { maxSide: 320 })
893
+ * .catch(() => placeholder),
894
+ * })),
895
+ * );
896
+ * plugin.windows.create("ui/results.slint").set({ rows });
897
+ * ```
898
+ *
899
+ * @since 1.5.19
900
+ */
901
+ remoteImage(url: string, options?: ImageOptions): Promise<ImageRef>;
811
902
  /**
812
903
  * Prepare a Markdown document for `MarkdownView` (`@vestra/markdown.slint`).
813
904
  *
@@ -3729,9 +3820,25 @@ export interface Player {
3729
3820
  * The core's first-tier relative seek (`seek-backward` / `seek-forward`,
3730
3821
  * ← / →): 0.1–3600 seconds in tenths, default 5. The newest call of any
3731
3822
  * plugin wins; it reverts when that plugin stops.
3823
+ *
3824
+ * `keyframes: true` (player 0.5.1, SDK 1.5.19): the step seeks
3825
+ * (`seek-backward` / `seek-forward`, including {@link Host.run} with
3826
+ * `seconds` and the picture-in-picture skips) land on a keyframe instead
3827
+ * of the exact time: forward on the first keyframe at or after position +
3828
+ * step, backward on the last at or before position − step, and they
3829
+ * always move. Presses during a seek add up and continue from where it
3830
+ * landed; the on-screen text shows the landed time. Sources without moving
3831
+ * video seek exactly. The seek bar, {@link Player.seek}, scrubs, chapters,
3832
+ * loops and {@link Player.step} are unaffected. Omitted or `false`: exact
3833
+ * seeks, as before. `seconds` and `keyframes` are written as a pair: the
3834
+ * newest call sets both.
3835
+ *
3836
+ * Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
3837
+ * the manifest when you rely on it.
3732
3838
  */
3733
3839
  setStepSeek(options: {
3734
3840
  seconds: number;
3841
+ keyframes?: boolean;
3735
3842
  }): Promise<void>;
3736
3843
  /**
3737
3844
  * The chapters of the current media (empty without media or chapters),
@@ -3770,22 +3877,28 @@ export interface Player {
3770
3877
  setAudioDevice(id: string | null): Promise<void>;
3771
3878
  }
3772
3879
  /**
3773
- * The core's basic playback actions (menu commands owned by the player).
3880
+ * The core's basic playback actions (menu commands owned by the player),
3881
+ * and the main window's fullscreen toggle.
3774
3882
  *
3775
3883
  * | action | default bindings | behaviour |
3776
3884
  * | --- | --- | --- |
3777
3885
  * | `stop` | F4 | close the source |
3778
- * | `play-pause` | Space | pause / resume, replay after the end |
3886
+ * | `play-pause` | Space, a left click on the video (player 0.5.2) | pause / resume, replay after the end |
3779
3887
  * | `previous` / `next` | PageUp, Mouse:X1 / PageDown, Mouse:X2 | ask the sequence provider |
3780
3888
  * | `frame-back` / `frame-forward` | D / F | `player.step("frame", …)` |
3781
- * | `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds |
3889
+ * | `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds (or `host.run`'s `seconds`), exact or to keyframes as `setStepSeek` says |
3782
3890
  * | `rate-normal`, `rate-down`, `rate-up` | Z, X, C | 1×, ±0.1 within 0.25–4 |
3783
3891
  * | `volume-up` / `volume-down` | ↑ / ↓, wheel | ±5 (`volume-up` stops at 100) |
3784
3892
  * | `mute` | M | toggle mute |
3893
+ * | `fullscreen` | Enter, Alt+Enter, F11 | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
3785
3894
  *
3786
- * In the binding overlay their keys are `host/<action>`.
3895
+ * In the binding overlay their keys are `host/<action>`. `fullscreen`
3896
+ * changes no playback state: read the result from `window.state.fullscreen`
3897
+ * (`plugin.window.onChange`), which also changes when the user toggles it.
3898
+ * Players before 0.5.2 reject it with `invalid`: set
3899
+ * `"minPlayerVersion": "0.5.2"` when you use it.
3787
3900
  */
3788
- export type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "frame-forward" | "seek-backward" | "seek-forward" | "rate-down" | "rate-up" | "rate-normal" | "volume-up" | "volume-down" | "mute";
3901
+ export type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "frame-forward" | "seek-backward" | "seek-forward" | "rate-down" | "rate-up" | "rate-normal" | "volume-up" | "volume-down" | "mute" | "fullscreen";
3789
3902
  /** `plugin.host`: the player's own core actions. */
3790
3903
  export interface Host {
3791
3904
  /**
@@ -3802,6 +3915,31 @@ export interface Host {
3802
3915
  * ```
3803
3916
  */
3804
3917
  run(action: HostAction): Promise<void>;
3918
+ /**
3919
+ * One step seek of `seconds` (0.1–3600 in tenths) instead of the
3920
+ * {@link Player.setStepSeek} step: the same checks, mode (exact or
3921
+ * keyframes), accumulation and on-screen text as ← / →. Use it for more
3922
+ * seek steps of your own (Ctrl+→ and the like) so that they add up with
3923
+ * the core's and land the same way.
3924
+ *
3925
+ * In keyframe mode it resolves once the seek has landed (so
3926
+ * `player.state.position` is current after the `await`); presses that add
3927
+ * up resolve together, the last with the text. In exact mode it resolves
3928
+ * once the seek is submitted, as before. `invalid` for `seconds` out of
3929
+ * range.
3930
+ *
3931
+ * @player 0.5.1 (set `"minPlayerVersion": "0.5.1"`: older players ignore
3932
+ * `seconds` and seek by the `setStepSeek` step)
3933
+ * @since 1.5.19
3934
+ *
3935
+ * @example
3936
+ * ```ts
3937
+ * bar.on("forward30", () => plugin.host.run("seek-forward", { seconds: 30 }));
3938
+ * ```
3939
+ */
3940
+ run(action: "seek-backward" | "seek-forward", options: {
3941
+ seconds: number;
3942
+ }): Promise<void>;
3805
3943
  }
3806
3944
  /**
3807
3945
  * This plugin's information block on the OSD: multi-line text over the
@@ -4844,6 +4982,26 @@ export interface Packages {
4844
4982
  * (players released with SDK 1.5.18 on; older ones disabled built-ins too).
4845
4983
  */
4846
4984
  setEnabled(id: string, enabled: boolean): Promise<void>;
4985
+ /**
4986
+ * Restart a plugin (`plugins.manage`): stop it and start it again from the
4987
+ * same package with the same data directory, for example to recover one
4988
+ * that misbehaves. Built-ins included. A stopped or failed plugin is just
4989
+ * started. The disabled flag is never written, so a reload never leaves
4990
+ * a plugin disabled.
4991
+ *
4992
+ * Resolves once the plugin is stopped and its start is scheduled; the
4993
+ * player starts it as soon as the old process is gone (at most 20 s).
4994
+ * Follow it with {@link Packages.onChange}: `starting`, then `running`,
4995
+ * or `failed` with the reason.
4996
+ *
4997
+ * `invalid` for oneself or a disabled plugin (enable it instead),
4998
+ * `missing` unknown, `busy` while it is being installed or reloaded.
4999
+ *
5000
+ * @player 0.5.1; older players reject it as an unknown request
5001
+ * (`unsupported`).
5002
+ * @since 1.5.19
5003
+ */
5004
+ reload(id: string): Promise<void>;
4847
5005
  /**
4848
5006
  * Remove an installed plugin (`plugins.manage`); `removeData` also deletes
4849
5007
  * its data directory, otherwise reinstalling it from the same source
@@ -1,6 +1,6 @@
1
1
  import type { Json } from "../wire.ts";
2
2
  import type { ScopeImpl } from "./scope.ts";
3
- import type { Dialogs, Disposable, ImageRef, Panels, PropValue, Props, Ui, View, Windows } from "./types.ts";
3
+ import type { Dialogs, Disposable, ImageOptions, ImageRef, Panels, PropValue, Props, Ui, View, Windows } from "./types.ts";
4
4
  /** Slint names are kebab-case; plugin code may use camelCase. */
5
5
  export declare const kebab: (name: string) => string;
6
6
  /**
@@ -13,11 +13,13 @@ export declare class ImageHandle implements ImageRef {
13
13
  #private;
14
14
  readonly path: string;
15
15
  readonly __vestraImage: true;
16
- constructor(scope: ScopeImpl, path: string);
16
+ constructor(scope: ScopeImpl, path: string, maxSide?: number);
17
17
  /** Released with its scope: writing it is `invalid`. */
18
18
  get released(): boolean;
19
19
  toJSON(): Json;
20
20
  }
21
+ /** `ImageOptions.maxSide`, checked: an integer 1–4096 or absent. */
22
+ export declare function imageSide(options: ImageOptions | undefined): number | undefined;
21
23
  export declare const ui: (scope: ScopeImpl) => Ui;
22
24
  /** Property and callback plumbing shared by windows, panels and settings pages. */
23
25
  export declare abstract class ViewBase implements View {
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,