@vestra-player/sdk 1.6.14 → 1.6.15

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.15
4
+
5
+ **Who must change: nobody.** A plugin may now mount one panel per slot (player 0.7.6) instead of one panel in all; plugins with one panel work as before, and `apiVersion` stays `vestra/1`.
6
+
7
+ - **One panel per slot.** `plugin.panels.mount` takes a second call for another slot: a playback plugin can have its control bar in `"bottom"` and its controls in `"adjust"`. Each `Panel` has its own bridge global, properties, callbacks, `state`, `show()`, `toggle()`, `setInteraction()` and menu; disposing one leaves the others, and stopping the plugin unmounts them all. A failed mount affects only that panel. A second panel in the same slot still throws (an `Error`, before anything is sent); the bottom bar and the backdrop still hold one panel for all plugins. Players before 0.7.6 refuse a second panel with `conflict` (they keep one panel per plugin); a plugin that mounts two declares `minPlayerVersion` 0.7.6.
8
+ - **Wire** (additive): `ui.setMount`, `ui.interaction`, `ui.menu` and `ui.unmount` take an optional `slot` naming the panel (without it: the plugin's only panel; `ui.unmount` without it removes every panel); `ui.callback` and `ui.preparing` carry the panel's `slot`, and `ui.callback` keeps `view: "$mount"`. Older players ignore `slot`.
9
+ - **`BUILTIN_COMMANDS`**: the player's seek, frame and speed actions (`host/seek-backward`, `seek-forward`, `frame-back`, `frame-forward`, `rate-down`, `rate-up`, `rate-normal`) leave Playback ▸ Seek and Speed for the command palette (player 0.7.6, with the playback plugin's panel); their keys and categories stay.
10
+ - **`TestHost`**: `panels` (by slot, in mount order) and `mounted(slot?)`; without a slot `mounted()` returns the only panel and throws when there are several. `panel` stays, as the last panel mounted. The handlers take `slot` as the player does; `ui.interaction` stores the locks without it.
3
11
  ## 1.6.14
4
12
 
5
13
  **Who must change: nobody.** A data refresh: `BUILTIN_COMMANDS` lists the built-in capture plugin's commands as the player registers them since its capture tab; no API change, `apiVersion` stays `vestra/1`.
package/dist/commands.js CHANGED
@@ -330,12 +330,8 @@ vestra.playback | copy-path | Copy to Clipboard ▸ Full Path
330
330
  host | play-pause | Playback ▸ Play/Pause | Space, Mouse:LeftClick
331
331
  host | previous | Playback ▸ Previous | PageUp, Mouse:X2
332
332
  host | next | Playback ▸ Next | PageDown, Mouse:X1
333
- host | frame-back | Playback ▸ Seek ▸ Previous Frame | D
334
- host | frame-forward | Playback ▸ Seek ▸ Next Frame | F
335
333
  vestra.playback | keyframe-back | Playback ▸ Seek ▸ Previous Keyframe | Ctrl+Shift+ArrowLeft
336
334
  vestra.playback | keyframe-forward | Playback ▸ Seek ▸ Next Keyframe | Ctrl+Shift+ArrowRight
337
- host | seek-backward | Playback ▸ Seek ▸ Back 5 s | ArrowLeft
338
- host | seek-forward | Playback ▸ Seek ▸ Forward 5 s | ArrowRight
339
335
  vestra.playback | backward-2 | Playback ▸ Seek ▸ Back 30 s | Ctrl+ArrowLeft
340
336
  vestra.playback | forward-2 | Playback ▸ Seek ▸ Forward 30 s | Ctrl+ArrowRight
341
337
  vestra.playback | backward-3 | Playback ▸ Seek ▸ Back 60 s | Shift+ArrowLeft
@@ -348,9 +344,6 @@ vestra.subtitles | dialogue-current | Playback ▸ Seek ▸ Current Subtitle Sta
348
344
  vestra.playback | jump-start | Playback ▸ Seek ▸ Jump to Start | Backspace
349
345
  vestra.playback | time-jump | Playback ▸ Seek ▸ Seek Steps…
350
346
  vestra.playback | jump | Playback ▸ Jump to Time… | G
351
- host | rate-normal | Playback ▸ Speed ▸ Normal | Z
352
- host | rate-down | Playback ▸ Speed ▸ Slower 0.1 | X
353
- host | rate-up | Playback ▸ Speed ▸ Faster 0.1 | C
354
347
  vestra.playback | rate-05 | Playback ▸ Speed ▸ 0.5×
355
348
  vestra.playback | rate-15 | Playback ▸ Speed ▸ 1.5×
356
349
  vestra.playback | rate-20 | Playback ▸ Speed ▸ 2×
@@ -594,6 +587,13 @@ host | exit-fullscreen | (Leave Fullscreen)
594
587
  host | sidebar | (Toggle Sidebar)
595
588
  host | palette | (Command Palette) | Ctrl+Shift+P
596
589
  host | adjust | (Adjust)
590
+ host | frame-back | (Previous Frame) | D
591
+ host | frame-forward | (Next Frame) | F
592
+ host | seek-backward | (Back 5 s) | ArrowLeft
593
+ host | seek-forward | (Forward 5 s) | ArrowRight
594
+ host | rate-normal | (Normal) | Z
595
+ host | rate-down | (Slower 0.1) | X
596
+ host | rate-up | (Faster 0.1) | C
597
597
  vestra.capture | capture.clipExport | (Export Clip Range) | Ctrl+Shift+E
598
598
  vestra.capture | capture.saveAs | (Save Current Frame As…)
599
599
  vestra.capture | capture.saveScreen | (Save Rendered Frame) | Ctrl+Alt+E
Binary file
@@ -248,7 +248,15 @@ export class Runtime {
248
248
  this.directory = directory;
249
249
  transport.on("ui.callback", (data) => {
250
250
  const event = data;
251
- this.views.get(event.view)?.(event.callback, event.args);
251
+ // A panel's callback names its slot (player 0.7.6: one panel per
252
+ // slot); older players name only `"$mount"`, and the plugin has one.
253
+ const view = event.view === "$mount"
254
+ ? event.slot
255
+ ? `panel:${event.slot}`
256
+ : [...this.views.keys()].find((key) => key.startsWith("panel:"))
257
+ : event.view;
258
+ if (view)
259
+ this.views.get(view)?.(event.callback, event.args);
252
260
  });
253
261
  transport.on("commands.execute", (data) => {
254
262
  this.commands.get(data.id)?.();
@@ -480,6 +480,37 @@ export interface MarkdownWire {
480
480
  /** Remote image address → the file the SDK downloaded for it. */
481
481
  images: Record<string, string>;
482
482
  }
483
+ /**
484
+ * One mounted panel of the plugin under test ({@link TestHost.mounted},
485
+ * {@link TestHost.panels}): a {@link FakeWindow} record with its slot,
486
+ * component and interaction locks.
487
+ *
488
+ * @remarks
489
+ * One per slot the plugin mounted (SDK 1.6.15), kept in
490
+ * {@link TestHost.panels}; disposing a panel removes only its own.
491
+ *
492
+ * @example Check the adjust panel's locks
493
+ * The plugin's adjust panel reports a drag; the test reads the locks it
494
+ * sent. `mounted(slot)` picks one of the plugin's panels by slot.
495
+ * ```ts
496
+ * import assert from "node:assert/strict";
497
+ * declare const host: TestHost;
498
+ * // ---cut---
499
+ * const panel: FakePanel = host.mounted("adjust");
500
+ * assert.deepEqual(panel.locks, { focused: false, dragging: true, popupOpen: false });
501
+ * ```n *
502
+ * @capability None.
503
+ * @since 1.6.15
504
+ * @category Testing
505
+ */
506
+ export type FakePanel = Omit<FakeWindow, "callbacks" | "userClose"> & {
507
+ /** `"bottom"`, `"sidebar"`, `"adjust"` or `"backdrop"`. */
508
+ slot: string;
509
+ /** The exported component the SDK found in the file. */
510
+ component: string;
511
+ /** The last `panel.setInteraction(...)` locks, `null` before any. */
512
+ locks: Json;
513
+ };
483
514
  /**
484
515
  * An in-memory fake Vestra Player for plugin tests.
485
516
  *
@@ -597,17 +628,16 @@ export declare class TestHost implements Transport {
597
628
  /** Tool windows by id (see {@link TestHost.window}). */
598
629
  readonly windows: Map<string, FakeWindow>;
599
630
  /**
600
- * The plugin's mounted panel (bottom bar, sidebar or backdrop), or `null`.
601
- * See {@link TestHost.mounted}.
631
+ * The plugin's mounted panels by slot, in mount order (SDK 1.6.15: one
632
+ * panel per slot). See {@link TestHost.mounted}.
602
633
  */
603
- panel: (Omit<FakeWindow, "callbacks" | "userClose"> & {
604
- /** `"bottom"`, `"sidebar"`, `"adjust"` or `"backdrop"`. */
605
- slot: string;
606
- /** The exported component the SDK found in the file. */
607
- component: string;
608
- /** The last `panel.setInteraction(...)` locks, `null` before any. */
609
- locks: Json;
610
- }) | null;
634
+ readonly panels: Map<string, FakePanel>;
635
+ /**
636
+ * The plugin's mounted panel (bottom bar, sidebar, adjust column or
637
+ * backdrop), the last mounted when it has several, or `null`. See
638
+ * {@link TestHost.mounted}.
639
+ */
640
+ get panel(): FakePanel | null;
611
641
  /** The plugin's commands by local id (see {@link TestHost.command}). */
612
642
  readonly commands: Map<string, FakeCommand>;
613
643
  /** The player's 14 core actions (`host/<id>`), kept apart from the plugin's `commands`. */
@@ -2814,9 +2844,11 @@ export declare class TestHost implements Transport {
2814
2844
  */
2815
2845
  window(id: string): FakeWindow;
2816
2846
  /**
2817
- * The plugin's mounted panel (bottom bar, sidebar or backdrop). Throws when
2818
- * none is mounted. Its `props` use Slint (kebab-case) names; `emit` fires a
2819
- * callback on the panel's global.
2847
+ * The plugin's mounted panel (bottom bar, sidebar, adjust column or
2848
+ * backdrop): the one in `slot`, or without it the only one (SDK 1.6.15: a
2849
+ * plugin mounts one panel per slot; with several, name the slot). Throws
2850
+ * when there is none. Its `props` use Slint (kebab-case) names; `emit`
2851
+ * fires a callback on the panel's global.
2820
2852
  *
2821
2853
  * @example Select a row in the plugin's sidebar panel
2822
2854
  * The test checks the panel sits in the sidebar and selects its third
@@ -2835,14 +2867,7 @@ export declare class TestHost implements Transport {
2835
2867
  * @capability None.
2836
2868
  * @since 1.0.0
2837
2869
  */
2838
- mounted(): Omit<FakeWindow, "callbacks" | "userClose"> & {
2839
- /** `"bottom"`, `"sidebar"`, `"adjust"` or `"backdrop"`. */
2840
- slot: string;
2841
- /** The exported component the SDK found in the file. */
2842
- component: string;
2843
- /** The last `panel.setInteraction(...)` locks, `null` before any. */
2844
- locks: Json;
2845
- };
2870
+ mounted(slot?: "bottom" | "sidebar" | "adjust" | "backdrop"): FakePanel;
2846
2871
  /**
2847
2872
  * The parameters of every request of one wire method, oldest first.
2848
2873
  *
@@ -330,6 +330,8 @@ const SLOT_LABELS = {
330
330
  backdrop: "video backdrop",
331
331
  adjust: "adjust column",
332
332
  };
333
+ /** The view a panel's writes are kept under (the player's, per slot). */
334
+ const mountView = (slot) => slot === "bottom" ? "$mount" : `$mount:${slot}`;
333
335
  /** Texts only the test host uses, with their Chinese of SDK 1.5.8. */
334
336
  const TEST_ZH_HANS = {
335
337
  "this site": "这个网站",
@@ -521,10 +523,18 @@ export class TestHost {
521
523
  /** Tool windows by id (see {@link TestHost.window}). */
522
524
  windows = new Map();
523
525
  /**
524
- * The plugin's mounted panel (bottom bar, sidebar or backdrop), or `null`.
525
- * See {@link TestHost.mounted}.
526
+ * The plugin's mounted panels by slot, in mount order (SDK 1.6.15: one
527
+ * panel per slot). See {@link TestHost.mounted}.
526
528
  */
527
- panel = null;
529
+ panels = new Map();
530
+ /**
531
+ * The plugin's mounted panel (bottom bar, sidebar, adjust column or
532
+ * backdrop), the last mounted when it has several, or `null`. See
533
+ * {@link TestHost.mounted}.
534
+ */
535
+ get panel() {
536
+ return [...this.panels.values()].at(-1) ?? null;
537
+ }
528
538
  /** The plugin's commands by local id (see {@link TestHost.command}). */
529
539
  commands = new Map();
530
540
  /** The player's 14 core actions (`host/<id>`), kept apart from the plugin's `commands`. */
@@ -3627,9 +3637,11 @@ export class TestHost {
3627
3637
  return window;
3628
3638
  }
3629
3639
  /**
3630
- * The plugin's mounted panel (bottom bar, sidebar or backdrop). Throws when
3631
- * none is mounted. Its `props` use Slint (kebab-case) names; `emit` fires a
3632
- * callback on the panel's global.
3640
+ * The plugin's mounted panel (bottom bar, sidebar, adjust column or
3641
+ * backdrop): the one in `slot`, or without it the only one (SDK 1.6.15: a
3642
+ * plugin mounts one panel per slot; with several, name the slot). Throws
3643
+ * when there is none. Its `props` use Slint (kebab-case) names; `emit`
3644
+ * fires a callback on the panel's global.
3633
3645
  *
3634
3646
  * @example Select a row in the plugin's sidebar panel
3635
3647
  * The test checks the panel sits in the sidebar and selects its third
@@ -3648,11 +3660,27 @@ export class TestHost {
3648
3660
  * @capability None.
3649
3661
  * @since 1.0.0
3650
3662
  */
3651
- mounted() {
3663
+ mounted(slot) {
3664
+ if (slot !== undefined) {
3665
+ const panel = this.panels.get(slot);
3666
+ if (!panel)
3667
+ throw new Error(`no panel mounted in the ${slot}`);
3668
+ return panel;
3669
+ }
3670
+ if (this.panels.size > 1)
3671
+ throw new Error(`panels are mounted in ${[...this.panels.keys()].join(", ")}: name the slot`);
3652
3672
  if (!this.panel)
3653
3673
  throw new Error("no panel mounted");
3654
3674
  return this.panel;
3655
3675
  }
3676
+ /** The panel a request is about: its `slot`, else the only one. */
3677
+ #panelFor(p) {
3678
+ const slot = typeof p.slot === "string" ? p.slot : undefined;
3679
+ const panel = slot ? this.panels.get(slot) : this.panel;
3680
+ if (!panel)
3681
+ throw new HostError("missing", "owner has no ready mount");
3682
+ return panel;
3683
+ }
3656
3684
  /**
3657
3685
  * The parameters of every request of one wire method, oldest first.
3658
3686
  *
@@ -4260,16 +4288,18 @@ export class TestHost {
4260
4288
  // A sidebar panel is never live (player 0.7.3): the current tab keeps
4261
4289
  // the plugin running as busy work, a background tab not at all. The
4262
4290
  // adjust column's current tab only while the column is open (0.7.6).
4263
- if (this.panel?.slot === "sidebar") {
4264
- if (this.layout.sidebar?.owner === 1)
4265
- reasons.push("its sidebar tab is the current tab");
4266
- }
4267
- else if (this.panel?.slot === "adjust") {
4268
- if (this.layout.adjust?.owner === 1 && this.layout.adjust.preferred)
4269
- reasons.push("its adjust tab is the current tab");
4291
+ for (const panel of this.panels.values()) {
4292
+ if (panel.slot === "sidebar") {
4293
+ if (this.layout.sidebar?.owner === 1)
4294
+ reasons.push("its sidebar tab is the current tab");
4295
+ }
4296
+ else if (panel.slot === "adjust") {
4297
+ if (this.layout.adjust?.owner === 1 && this.layout.adjust.preferred)
4298
+ reasons.push("its adjust tab is the current tab");
4299
+ }
4300
+ else
4301
+ reasons.push("a mounted panel");
4270
4302
  }
4271
- else if (this.panel)
4272
- reasons.push("a mounted panel");
4273
4303
  for (const [id, window] of this.windows)
4274
4304
  if (window.visible)
4275
4305
  reasons.push(`an open window (${id})`);
@@ -4501,10 +4531,11 @@ export class TestHost {
4501
4531
  throw new HostError("missing", "view not found");
4502
4532
  },
4503
4533
  "ui.mount": (p) => {
4504
- if (this.panel)
4505
- throw new HostError("conflict", "owner already mounted");
4506
4534
  if (!(String(p.slot) in SLOT_LABELS))
4507
4535
  throw new HostError("invalid", "unknown mount slot");
4536
+ // One panel per slot and plugin (player 0.7.6).
4537
+ if (this.panels.has(String(p.slot)))
4538
+ throw new HostError("conflict", "this plugin already has a panel in this slot");
4508
4539
  // Like the player: one panel in the bottom bar and the backdrop, any
4509
4540
  // number of tabs in the sidebar (player 0.7.3; 0.4.x to 0.7.2 took 8)
4510
4541
  // and in the adjust column (player 0.7.6).
@@ -4520,10 +4551,11 @@ export class TestHost {
4520
4551
  ]));
4521
4552
  const properties = this.#initialValues(this.#shownValues(p.properties));
4522
4553
  this.#seeImages(properties);
4523
- this.#resetWrites("$mount");
4554
+ const panelSlot = String(p.slot);
4555
+ this.#resetWrites(mountView(panelSlot));
4524
4556
  const host = this;
4525
- this.panel = {
4526
- slot: String(p.slot),
4557
+ this.panels.set(panelSlot, {
4558
+ slot: panelSlot,
4527
4559
  file: String(p.file),
4528
4560
  component: String(p.component),
4529
4561
  visible: true,
@@ -4532,11 +4564,12 @@ export class TestHost {
4532
4564
  emit(callback, ...args) {
4533
4565
  host.emit("ui.callback", {
4534
4566
  view: "$mount",
4567
+ slot: panelSlot,
4535
4568
  callback: kebab(callback),
4536
4569
  args,
4537
4570
  });
4538
4571
  },
4539
- };
4572
+ });
4540
4573
  if (p.slot === "backdrop") {
4541
4574
  this.backdrop = { owner: this.manifestId, effective: true };
4542
4575
  return { status: "ready", prepareMs: 0, shellGeneration: 1 };
@@ -4551,18 +4584,24 @@ export class TestHost {
4551
4584
  });
4552
4585
  return { status: "ready", prepareMs: 0, shellGeneration: 1 };
4553
4586
  },
4554
- "ui.unmount": () => {
4555
- if (this.panel?.slot === "backdrop")
4556
- this.backdrop = null;
4557
- else if (this.panel)
4558
- this.#removePanel(this.panel.slot, 1);
4559
- this.panel = null;
4587
+ "ui.unmount": (p) => {
4588
+ // The slot's panel, or without one every panel (as the player).
4589
+ const slots = typeof p.slot === "string" ? [p.slot] : [...this.panels.keys()];
4590
+ for (const slot of slots) {
4591
+ if (!this.panels.delete(slot))
4592
+ continue;
4593
+ if (slot === "backdrop")
4594
+ this.backdrop = null;
4595
+ else
4596
+ this.#removePanel(slot, 1);
4597
+ }
4560
4598
  },
4561
4599
  "ui.menu": (p) => {
4562
- if (!this.panel)
4600
+ if (!this.panels.size)
4563
4601
  throw new HostError("unavailable", "panel not shown");
4564
- const tabs = this.panel.slot === "sidebar" || this.panel.slot === "adjust"
4565
- ? this.layout[this.panel.slot]
4602
+ const panel = this.#panelFor(p);
4603
+ const tabs = panel.slot === "sidebar" || panel.slot === "adjust"
4604
+ ? this.layout[panel.slot]
4566
4605
  : null;
4567
4606
  if (tabs && tabs.owner !== 1)
4568
4607
  throw new HostError("unavailable", "the panel's tab is not the current one");
@@ -4613,8 +4652,9 @@ export class TestHost {
4613
4652
  : collapsed
4614
4653
  ? "fullscreen"
4615
4654
  : null;
4616
- if (this.panel?.slot === slot)
4617
- this.panel.visible = p.visible;
4655
+ const panel = this.panels.get(slot);
4656
+ if (panel)
4657
+ panel.visible = p.visible;
4618
4658
  }
4619
4659
  if (typeof p.detached === "boolean")
4620
4660
  state.detached = p.detached;
@@ -4880,9 +4920,9 @@ export class TestHost {
4880
4920
  this.emit("sequence.step", { direction });
4881
4921
  },
4882
4922
  "ui.setMount": (p) => {
4883
- const panel = this.mounted();
4923
+ const panel = this.#panelFor(p);
4884
4924
  const property = String(p.property);
4885
- const next = this.#write("$mount", property, p, panel.props[property]);
4925
+ const next = this.#write(mountView(panel.slot), property, p, panel.props[property]);
4886
4926
  // A part of a value sent in parts is held until the last (player 0.6.0).
4887
4927
  if (next !== undefined)
4888
4928
  panel.props[property] = next;
@@ -5122,7 +5162,10 @@ export class TestHost {
5122
5162
  })));
5123
5163
  return { applied: true };
5124
5164
  },
5125
- "ui.interaction": (p) => void (this.mounted().locks = p),
5165
+ "ui.interaction": (p) => {
5166
+ const { slot: _, ...locks } = p;
5167
+ this.#panelFor(p).locks = locks;
5168
+ },
5126
5169
  "dialogs.confirm": (p) => {
5127
5170
  this.confirms.push(p);
5128
5171
  const shown = (field, formatted = false) => p[field] === undefined
@@ -4095,9 +4095,14 @@ export interface BackdropSlot {
4095
4095
  * or the sidebar moves to or from its floating window, while the global is
4096
4096
  * one stable address.
4097
4097
  *
4098
- * Each plugin mounts at most one panel, and each position holds one panel
4099
- * for all plugins. {@link Panels.bottom} and {@link Panels.sidebar} describe
4100
- * the positions, whoever mounted there.
4098
+ * A plugin mounts at most one panel per slot (SDK 1.6.15, player 0.7.6;
4099
+ * before, one panel in all): a playback plugin can have its control bar in
4100
+ * `"bottom"` and its controls in `"adjust"`. Each {@link Panel} has its own
4101
+ * bridge, state, `toggle()` and `setInteraction()`; disposing one leaves
4102
+ * the others, and stopping the plugin unmounts them all. The bottom bar and
4103
+ * the backdrop hold one panel for all plugins; the sidebar and the adjust
4104
+ * column one tab per plugin. {@link Panels.bottom}, {@link Panels.sidebar}
4105
+ * and {@link Panels.adjust} describe the positions, whoever mounted there.
4101
4106
  *
4102
4107
  * @capability ui.mount
4103
4108
  *
@@ -4194,8 +4199,11 @@ export interface Panels {
4194
4199
  * @returns A {@link Panel} owned by this scope; await {@link Panel.ready}
4195
4200
  * for the result.
4196
4201
  *
4197
- * @throws `Error` without a code (throws) This plugin already has a panel,
4198
- * or the file exports no component or global, or several and `options`
4202
+ * @throws `Error` without a code (throws) This plugin already has a panel
4203
+ * in this slot (a panel in another slot is fine from player 0.7.6;
4204
+ * players before 0.7.6 refuse a second panel with `conflict`, so a
4205
+ * plugin that mounts two declares `minPlayerVersion` 0.7.6), or the file
4206
+ * exports no component or global, or several and `options`
4199
4207
  * does not name one. A file that cannot be read throws Node's file system
4200
4208
  * error (`code` `ENOENT` and so on).
4201
4209
  * @throws The errors of {@link Panel.ready} (rejects `ready`) The player
package/dist/plugin/ui.js CHANGED
@@ -839,13 +839,15 @@ class PanelHandle extends ViewBase {
839
839
  #locks = { focused: false, dragging: false, popupOpen: false };
840
840
  #interaction;
841
841
  constructor(scope, slot, file, options) {
842
- if (scope.runtime.views.has("$mount"))
843
- throw new Error("a plugin can mount only one panel");
842
+ // One panel per slot (SDK 1.6.15, player 0.7.6; a player before 0.7.6
843
+ // refuses a second panel with `conflict`).
844
+ if (scope.runtime.views.has(`panel:${slot}`))
845
+ throw new Error(`this plugin already has a panel in the ${slot}`);
844
846
  super(scope);
845
847
  this.#slot = slot;
846
848
  const runtime = scope.runtime;
847
- runtime.views.set("$mount", (callback, args) => this.dispatch(callback, args));
848
- this.#interaction = new Latest((locks) => runtime.host.ui.interaction(locks));
849
+ runtime.views.set(`panel:${slot}`, (callback, args) => this.dispatch(callback, args));
850
+ this.#interaction = new Latest((locks) => runtime.host.ui.interaction(locks, this.#named()));
849
851
  const component = options.component ?? exported(runtime.directory, file, "component");
850
852
  const global = options.global ?? exported(runtime.directory, file, "global");
851
853
  scope.add(this);
@@ -874,8 +876,17 @@ class PanelHandle extends ViewBase {
874
876
  live() {
875
877
  return this.#mounted && !this.closed;
876
878
  }
879
+ /**
880
+ * The slot to name in a request about this panel: only while the plugin
881
+ * has panels in more than one slot (the player then needs it); a plugin
882
+ * with one panel sends what it always sent.
883
+ */
884
+ #named() {
885
+ const panels = [...this.scope.runtime.views.keys()].filter((key) => key.startsWith("panel:"));
886
+ return panels.length > 1 ? this.#slot : undefined;
887
+ }
877
888
  write(property, write) {
878
- return this.scope.runtime.host.ui.setMount(property, write);
889
+ return this.scope.runtime.host.ui.setMount(property, write, this.#named());
879
890
  }
880
891
  get state() {
881
892
  const service = layoutOf(this.scope.runtime);
@@ -932,7 +943,7 @@ class PanelHandle extends ViewBase {
932
943
  }
933
944
  async showMenu(items, at) {
934
945
  await this.ready;
935
- await this.scope.runtime.host.ui.menu(items.map((item) => item?.id ?? null), at.x, at.y);
946
+ await this.scope.runtime.host.ui.menu(items.map((item) => item?.id ?? null), at.x, at.y, this.#named());
936
947
  }
937
948
  onDrop(listener) {
938
949
  const watch = dropsOf(this.scope.runtime).listen(this.scope, (event) => this.scope.run(listener, event));
@@ -950,10 +961,10 @@ class PanelHandle extends ViewBase {
950
961
  this.scope.forget(this);
951
962
  this.#interaction.stop();
952
963
  if (this.#mounted)
953
- await this.scope.runtime.host.ui.unmount().catch(() => { });
964
+ await this.scope.runtime.host.ui.unmount(this.#named()).catch(() => { });
954
965
  this.#mounted = false;
955
966
  await this.release();
956
- this.scope.runtime.views.delete("$mount");
967
+ this.scope.runtime.views.delete(`panel:${this.#slot}`);
957
968
  // Listeners of `onChange` hear `null`.
958
969
  this.scope.runtime.invalidate();
959
970
  await layoutOf(this.scope.runtime).refresh();
package/dist/wire.d.ts CHANGED
@@ -275,6 +275,8 @@ export interface WireTimeline {
275
275
  marks: WireMark[];
276
276
  ranges?: WireRange[];
277
277
  }
278
+ /** A panel position (`ui.mount`'s `slot`). */
279
+ export type MountSlot = "bottom" | "sidebar" | "backdrop" | "adjust";
278
280
  /** The backdrop as `layout.state` reports it; `null` without one. */
279
281
  export interface LayoutBackdrop {
280
282
  owner: number;
@@ -752,14 +754,14 @@ export declare function createWire(transport: Transport): {
752
754
  prepareMs: number;
753
755
  shellGeneration: number;
754
756
  }>;
755
- setMount: (property: string, write: PropertyWrite) => Promise<void>;
757
+ setMount: (property: string, write: PropertyWrite, slot?: MountSlot) => Promise<void>;
756
758
  interaction: (locks: {
757
759
  focused: boolean;
758
760
  dragging: boolean;
759
761
  popupOpen: boolean;
760
- }) => Promise<void>;
761
- unmount: () => Promise<void>;
762
- menu: (items: (string | null)[], x: number, y: number) => Promise<void>;
762
+ }, slot?: MountSlot) => Promise<void>;
763
+ unmount: (slot?: MountSlot) => Promise<void>;
764
+ menu: (items: (string | null)[], x: number, y: number, slot?: MountSlot) => Promise<void>;
763
765
  open: (id: string, file: string, properties?: Record<string, Json>, callbacks?: string[]) => Promise<void>;
764
766
  set: (id: string, property: string, write: PropertyWrite) => Promise<void>;
765
767
  show: (id: string, visible: boolean) => Promise<void>;
package/dist/wire.js CHANGED
@@ -328,10 +328,24 @@ export function createWire(transport) {
328
328
  base,
329
329
  }),
330
330
  mount: (options) => transport.request("ui.mount", options),
331
- setMount: (property, write) => transport.request("ui.setMount", { property, ...write }),
332
- interaction: (locks) => transport.request("ui.interaction", locks),
333
- unmount: () => transport.request("ui.unmount"),
334
- menu: (items, x, y) => transport.request("ui.menu", { items, x, y }),
331
+ // `slot`: which of the plugin's panels (player 0.7.6: one per slot;
332
+ // older players have one and ignore it).
333
+ setMount: (property, write, slot) => transport.request("ui.setMount", {
334
+ property,
335
+ ...write,
336
+ ...(slot ? { slot } : {}),
337
+ }),
338
+ interaction: (locks, slot) => transport.request("ui.interaction", {
339
+ ...locks,
340
+ ...(slot ? { slot } : {}),
341
+ }),
342
+ unmount: (slot) => transport.request("ui.unmount", slot ? { slot } : undefined),
343
+ menu: (items, x, y, slot) => transport.request("ui.menu", {
344
+ items,
345
+ x,
346
+ y,
347
+ ...(slot ? { slot } : {}),
348
+ }),
335
349
  open: (id, file, properties = {}, callbacks = []) => transport.request("ui.open", { id, file, properties, callbacks }),
336
350
  set: (id, property, write) => transport.request("ui.set", { id, property, ...write }),
337
351
  show: (id, visible) => transport.request("ui.show", { id, visible }),
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.6.14: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
3
+ > @vestra-player/sdk 1.6.15: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
4
4
 
5
5
  ## Read the documentation
6
6
 
@@ -38,5 +38,5 @@
38
38
  - `/API/Sources`: 7 entries: ResolverOptions, WebResolverOptions, FileResolverOptions, ResolvedStream, Resolver, Resolvers, ResolvedMedia
39
39
  - `/API/Subtitles`: 5 entries: SubtitleTrack, SubtitlesState, SubtitleConditional, Subtitles, SubtitleStyle
40
40
  - `/API/UI`: 44 entries: ImageRef, ImageOptions, MarkdownRef, MarkdownOptions, PropValue, PropData, Props, Ui, View, Window, Windows, SlotState, SlotPanel, PanelState, PanelSlot, SidebarSlot, Panel,…
41
- - `/API/Testing`: 33 entries: CapabilityInfo, CAPABILITY_INFO, MESSAGE_LIMIT, COMMAND_LIMITS, MENU_IDS, BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BuiltinSubmenu, BUILTIN_SUBMENUS, BuiltinCommand, BUILTIN_COMMANDS, LOCALES,…
41
+ - `/API/Testing`: 34 entries: CapabilityInfo, CAPABILITY_INFO, MESSAGE_LIMIT, COMMAND_LIMITS, MENU_IDS, BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BuiltinSubmenu, BUILTIN_SUBMENUS, BuiltinCommand, BUILTIN_COMMANDS, LOCALES,…
42
42
  - `/API/Docs`: 4 entries: DOCS_PAGE_BYTES, DocsOptions, docs, searchDocs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.6.14",
3
+ "version": "1.6.15",
4
4
  "description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {