@vestra-player/sdk 1.5.28 → 1.5.29

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.29
4
+
5
+ **Who must change: nobody.** Additions only, `apiVersion` stays `vestra/1`. A plugin that does not use `button` is unaffected. A plugin that uses it on a player before 0.5.10 keeps working without a button: the player builds the `plugin` object with its own SDK copy, which does not send the field, so the command and its menu entry register as before (declare `"minPlayerVersion": "0.5.10"` when the button matters). A plugin that mounts its own bottom bar should place `BarButtons` (below); until it does, `vestra-plugin check` warns and the player logs the buttons it hides.
6
+
7
+ - **Commands can have a bottom-bar button** (player 0.5.10, user-approved design of 2026-10-27): `button: { icon, order }` on `commands.add` (`CommandOptions.button`, `CommandButton`, `BarButtonIcon`). The icon is a name of the player's icon set or an image from `plugin.ui.image` (drawn in the icon colour); `order` 0–999 places it left to right. The button needs a menu `path`; its tooltip is the command's title (without a trailing "…") and first key; disabled or busy commands show a disabled button with the reason; a click runs the command like its menu item. One button per plugin, eight in the player (`BAR_BUTTON_LIMITS`; past eight the highest orders stay and the lowest go first, ties: the smaller plugin id, then the earlier declaration); a button past them is left out (the command and its menu entry stay) and the player logs it with `limit`. `add` throws `invalid` for an unknown icon name (`BAR_BUTTON_ICONS`), an order outside 0–999, a released image or a button without a path.
8
+ - **`@vestra/bar-buttons.slint`**: `BarButtons`, the strip of command buttons a bottom bar places at its right end with `room`, the width it can spare. It shows the cells that fit (the player's sidebar toggle, **Toggle Sidebar** `host/sidebar`, rightmost and last to go; cells go from the strip's outer (left) end inward, so of the plugins' buttons the lowest order goes first; `order` is position only) and is as wide as the cells it shows (`first-shown`, out, is the index of the leftmost plugin button shown). The player fills it in every window it composes and runs the clicks. Slint library copy synced; the icon set gains `store` (a shopping bag) and `ai` (a sparkle).
9
+ - **`TestHost.barButtons`**: the plugin's button as the player shows it (`command`, `icon`, `image`, `order`, `enabled`, `checked`, `tooltip`) with `click()`, which runs the command and rejects while it is disabled or once it is gone; empty after the command is disposed. The test host checks buttons as the player does.
10
+ - **`vestra-plugin check` warns** when a plugin mounts a bottom bar whose Slint file does not import `BarButtons` from `@vestra/bar-buttons.slint`, itself or through the plugin's own Slint files it imports (comments do not count; a warning, the check still passes).
11
+ - `BUILTIN_COMMANDS` gains the player's **Toggle Sidebar** (`host/sidebar`, outside the menu, no default key).
12
+ - Docs: *Commands* (concepts) gains *Bottom-bar buttons*; *Commands, menus and bindings* (reference) gains *Bottom-bar buttons*; *Interface guidelines* gains *Writing a bottom bar*; *Limits* gains *Bottom-bar buttons*; *Theme* gains *Bar buttons* and the two icons; *Windows, panels…* points to buttons for a one-click entry point. `llms.txt` and `llms-full.txt` regenerated.
13
+
3
14
  ## 1.5.28
4
15
 
5
16
  **Who must change: nobody.** No API changes, `apiVersion` stays `vestra/1`. Only the SDK's copy of what the player ships changes; a plugin that read `BUILTIN_COMMANDS` or `BUILTIN_SUBMENUS` to avoid a key or a menu path simply finds those entries free now.
@@ -75,6 +75,7 @@ import {
75
75
  basename,
76
76
  dirname,
77
77
  extname,
78
+ isAbsolute,
78
79
  join,
79
80
  relative,
80
81
  resolve,
@@ -356,6 +357,8 @@ async function check(target, report = null) {
356
357
  }
357
358
  say(`${target.name}: capabilities declared for every API use`);
358
359
  if (report) return;
360
+ for (const line of await bottomBarWarnings(target))
361
+ console.log(`${target.name}: warning: ${line}`);
359
362
  // Translation warnings never fail the check.
360
363
  if (present(join(target.directory, "locales"))) {
361
364
  try {
@@ -399,6 +402,106 @@ async function capabilityFindings(target, config) {
399
402
  .map((finding) => ({ ...finding, file: resolve(finding.file) }));
400
403
  }
401
404
 
405
+ /** Slint source without its comments (string literals kept as they are). */
406
+ function slintCode(text) {
407
+ let out = "";
408
+ for (let i = 0; i < text.length;) {
409
+ if (text[i] === '"') {
410
+ let j = i + 1;
411
+ while (j < text.length && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
412
+ out += text.slice(i, j + 1);
413
+ i = j + 1;
414
+ } else if (text.startsWith("//", i)) {
415
+ const next = text.indexOf("\n", i);
416
+ i = next < 0 ? text.length : next;
417
+ } else if (text.startsWith("/*", i)) {
418
+ const close = text.indexOf("*/", i + 2);
419
+ i = close < 0 ? text.length : close + 2;
420
+ out += " ";
421
+ } else out += text[i++];
422
+ }
423
+ return out;
424
+ }
425
+
426
+ /** Whether `path` lies inside `directory` (not a sibling sharing its prefix). */
427
+ function inside(directory, path) {
428
+ const rel = relative(resolve(directory), path);
429
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
430
+ }
431
+
432
+ /**
433
+ * Whether the Slint file at `file` (relative to the plugin directory)
434
+ * imports `BarButtons` from `@vestra/bar-buttons.slint`, itself or
435
+ * through the plugin's own Slint files (relative paths, any depth, inside
436
+ * the plugin directory). Local re-exports count as such files too:
437
+ * `export { Row } from "row.slint"` and `export * from "row.slint"` are
438
+ * followed, and `export { BarButtons } from "@vestra/bar-buttons.slint"` (or
439
+ * `export * from` it) in a file the bar imports counts as placing it.
440
+ * Comments do not count.
441
+ */
442
+ async function importsBarButtons(target, file, seen = new Set()) {
443
+ const path = resolve(target.directory, file);
444
+ if (seen.has(path) || !inside(target.directory, path)) return false;
445
+ seen.add(path);
446
+ const text = await readFile(path, "utf8").catch(() => null);
447
+ if (text === null) return false;
448
+ const code = slintCode(text);
449
+ // { names (or "*"), from }: imports, and re-exports with a source file.
450
+ const imports = [
451
+ ...code.matchAll(
452
+ /\b(?:import|export)\s*(?:(\*)\s*from\s*|\{([^}]*)\}\s*from\s*)?"([^"]+)"/g,
453
+ ),
454
+ ].map(([, star, names, from]) => ({ names: star ?? names ?? "", from }));
455
+ for (const { names, from } of imports)
456
+ if (
457
+ from === "@vestra/bar-buttons.slint" &&
458
+ (names === "*" ||
459
+ names
460
+ .split(",")
461
+ .some((name) => /^BarButtons(\s+as\s+[\w-]+)?$/.test(name.trim())))
462
+ )
463
+ return true;
464
+ for (const { from } of imports) {
465
+ if (
466
+ from.startsWith("@") ||
467
+ !from.endsWith(".slint") ||
468
+ from === "std-widgets.slint"
469
+ )
470
+ continue;
471
+ if (await importsBarButtons(target, join(dirname(file), from), seen))
472
+ return true;
473
+ }
474
+ return false;
475
+ }
476
+
477
+ /**
478
+ * A plugin that mounts a bottom bar (`panels.mount("bottom", "<file>")`)
479
+ * whose Slint file does not import `BarButtons` (`@vestra/bar-buttons.slint`),
480
+ * itself or through its local imports: while it is the bottom bar, no
481
+ * plugin's command button shows (SDK 1.5.29). The mount is read from the
482
+ * source text: a literal file name in the call.
483
+ */
484
+ async function bottomBarWarnings(target) {
485
+ const { typescript: sources } = await sourceFiles(target);
486
+ const files = new Set();
487
+ for (const file of sources) {
488
+ const text = await readFile(join(target.directory, file), "utf8");
489
+ for (const match of text.matchAll(
490
+ /\.mount\(\s*["'`]bottom["'`]\s*,\s*["'`]([^"'`$]+)["'`]/g,
491
+ ))
492
+ files.add(match[1]);
493
+ }
494
+ const warnings = [];
495
+ for (const file of files) {
496
+ if (!present(join(target.directory, file))) continue;
497
+ if (await importsBarButtons(target, file)) continue;
498
+ warnings.push(
499
+ `${file} is a bottom bar without BarButtons (import { BarButtons } from "@vestra/bar-buttons.slint"): while it is mounted, the command buttons of every plugin (and the player's sidebar toggle) show nowhere; see docs/guidelines/interface.md#writing-a-bottom-bar`,
500
+ );
501
+ }
502
+ return warnings;
503
+ }
504
+
402
505
  /** The plugin's own production .ts / .mts and .slint files. */
403
506
  async function sourceFiles(target) {
404
507
  const all = await files(target.directory);
@@ -1,5 +1,5 @@
1
1
  /** Finite candidate command registry. Host authenticates owner; no plugin supplies an owner id. */
2
- import type { Transport } from "./wire.ts";
2
+ import type { Json, Transport } from "./wire.ts";
3
3
  import type { ListJson, TextJson } from "./i18n.ts";
4
4
  /**
5
5
  * The player's command registry caps, the same values as the player's own
@@ -44,6 +44,43 @@ export declare const COMMAND_LIMITS: {
44
44
  * ```
45
45
  */
46
46
  export declare const MENU_IDS: readonly ["open", "playback", "subtitles", "video", "audio", "screen", "settings"];
47
+ /**
48
+ * The icons a bottom-bar button may name (`CommandOptions.button.icon`):
49
+ * every icon of the player's V-Line set, `@vestra/icons.slint` (draw one
50
+ * in your own Slint with `Icon { name: "store"; }`). The player's tests check
51
+ * that this list is its icon set.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import assert from "node:assert/strict";
56
+ * import { BAR_BUTTON_ICONS } from "@vestra-player/sdk/testing";
57
+ *
58
+ * assert.ok(BAR_BUTTON_ICONS.includes("store"));
59
+ * ```
60
+ *
61
+ * @since 1.5.29
62
+ */
63
+ export declare const BAR_BUTTON_ICONS: readonly ["play", "pause", "stop", "back", "forward", "open", "volume", "mute", "playlist", "detach", "attach", "close", "failed", "move-top", "move-up", "move-down", "move-bottom", "search", "chapter", "bookmark", "check-task", "check-box", "menu-check", "submenu", "scroll-up", "scroll-down", "caret-down", "sort-asc", "sort-desc", "tag-remove", "preview-play", "add", "remove", "sort", "fullscreen", "exit-fullscreen", "store", "ai"];
64
+ /**
65
+ * The player's limits on bottom-bar buttons (`CommandOptions.button`), the
66
+ * same values as the player's (its tests check that they agree). A button
67
+ * past `perPlugin` or `total` is left out: its command still registers and
68
+ * its menu entry stays, and the player logs the button it left out
69
+ * (`limit`). Which ones stay: each plugin's first declared button, then the
70
+ * `total` highest by order, plugin id and declaration order: as in a narrow
71
+ * strip, the lowest order goes first (equal orders: the smaller plugin id,
72
+ * then the earlier declaration, goes first).
73
+ *
74
+ * @since 1.5.29
75
+ */
76
+ export declare const BAR_BUTTON_LIMITS: {
77
+ /** Buttons one plugin shows (1). */
78
+ readonly perPlugin: 1;
79
+ /** Buttons the player shows, every plugin together, besides its own sidebar toggle (8). */
80
+ readonly total: 8;
81
+ /** The highest `order` (999; the lowest is 0). */
82
+ readonly maxOrder: 999;
83
+ };
47
84
  /**
48
85
  * A submenu that one of the player's built-in plugins declares: an entry of
49
86
  * {@link BUILTIN_SUBMENUS}.
@@ -157,6 +194,15 @@ export type CommandDeclaration = {
157
194
  focus: "panel" | null;
158
195
  /** Fire again on keyboard auto-repeat while held. */
159
196
  repeat: boolean;
197
+ /**
198
+ * A bottom-bar button (player 0.5.10): an icon name, or an image in the
199
+ * `plugin.ui.image` wire form. Sent only when set: older players refuse
200
+ * unknown declaration fields.
201
+ */
202
+ button?: {
203
+ icon: string | Json;
204
+ order: number;
205
+ };
160
206
  };
161
207
  export declare function commandClient(transport: Transport): {
162
208
  register: (id: string, revision: number, declaration: CommandDeclaration, state: CommandState) => Promise<{
package/dist/commands.js CHANGED
@@ -49,6 +49,82 @@ export const MENU_IDS = [
49
49
  "screen",
50
50
  "settings",
51
51
  ];
52
+ /**
53
+ * The icons a bottom-bar button may name (`CommandOptions.button.icon`):
54
+ * every icon of the player's V-Line set, `@vestra/icons.slint` (draw one
55
+ * in your own Slint with `Icon { name: "store"; }`). The player's tests check
56
+ * that this list is its icon set.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import assert from "node:assert/strict";
61
+ * import { BAR_BUTTON_ICONS } from "@vestra-player/sdk/testing";
62
+ *
63
+ * assert.ok(BAR_BUTTON_ICONS.includes("store"));
64
+ * ```
65
+ *
66
+ * @since 1.5.29
67
+ */
68
+ export const BAR_BUTTON_ICONS = [
69
+ "play",
70
+ "pause",
71
+ "stop",
72
+ "back",
73
+ "forward",
74
+ "open",
75
+ "volume",
76
+ "mute",
77
+ "playlist",
78
+ "detach",
79
+ "attach",
80
+ "close",
81
+ "failed",
82
+ "move-top",
83
+ "move-up",
84
+ "move-down",
85
+ "move-bottom",
86
+ "search",
87
+ "chapter",
88
+ "bookmark",
89
+ "check-task",
90
+ "check-box",
91
+ "menu-check",
92
+ "submenu",
93
+ "scroll-up",
94
+ "scroll-down",
95
+ "caret-down",
96
+ "sort-asc",
97
+ "sort-desc",
98
+ "tag-remove",
99
+ "preview-play",
100
+ "add",
101
+ "remove",
102
+ "sort",
103
+ "fullscreen",
104
+ "exit-fullscreen",
105
+ "store",
106
+ "ai",
107
+ ];
108
+ /**
109
+ * The player's limits on bottom-bar buttons (`CommandOptions.button`), the
110
+ * same values as the player's (its tests check that they agree). A button
111
+ * past `perPlugin` or `total` is left out: its command still registers and
112
+ * its menu entry stays, and the player logs the button it left out
113
+ * (`limit`). Which ones stay: each plugin's first declared button, then the
114
+ * `total` highest by order, plugin id and declaration order: as in a narrow
115
+ * strip, the lowest order goes first (equal orders: the smaller plugin id,
116
+ * then the earlier declaration, goes first).
117
+ *
118
+ * @since 1.5.29
119
+ */
120
+ export const BAR_BUTTON_LIMITS = {
121
+ /** Buttons one plugin shows (1). */
122
+ perPlugin: 1,
123
+ /** Buttons the player shows, every plugin together, besides its own sidebar toggle (8). */
124
+ total: 8,
125
+ /** The highest `order` (999; the lowest is 0). */
126
+ maxOrder: 999,
127
+ };
52
128
  /**
53
129
  * The submenus the player's built-in plugins declare, by key path, in menu
54
130
  * order (built-in menus as in {@link MENU_IDS}, then top-level submenus).
@@ -413,6 +489,7 @@ vestra.preferences | language.es | Settings ▸ Language ▸ Español
413
489
  vestra.info | info.about | About Vestra Player… | F1
414
490
  host | quit | Quit | Cmd+Q
415
491
  host | exit-fullscreen | (Leave Fullscreen) | Escape
492
+ host | sidebar | (Toggle Sidebar)
416
493
  vestra.info | info.brief | (Brief Info) | Shift+Tab
417
494
  vestra.playlist | playlist.play | (Play) | Enter, P | panel
418
495
  vestra.playlist | playlist.list-add | (Add Files…) | Ctrl+I | panel
@@ -13,7 +13,7 @@ export { HostError } from "./wire.ts";
13
13
  export type { Json, Transport } from "./wire.ts";
14
14
  export { API_VERSION, CAPABILITIES, CAPABILITY_INFO, MESSAGE_LIMIT, } from "./capabilities.ts";
15
15
  export { HOST_CAPABILITIES } from "./host-capabilities.ts";
16
- export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.ts";
16
+ export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.ts";
17
17
  export { BINDING_INPUTS } from "./plugin/bindings.ts";
18
18
  export { OSD_LIMITS } from "./plugin/osd.ts";
19
19
  export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.ts";
@@ -11,7 +11,7 @@ export { createPlugin } from "./plugin/index.js";
11
11
  export { HostError } from "./wire.js";
12
12
  export { API_VERSION, CAPABILITIES, CAPABILITY_INFO, MESSAGE_LIMIT, } from "./capabilities.js";
13
13
  export { HOST_CAPABILITIES } from "./host-capabilities.js";
14
- export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.js";
14
+ export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "./commands.js";
15
15
  export { BINDING_INPUTS } from "./plugin/bindings.js";
16
16
  export { OSD_LIMITS } from "./plugin/osd.js";
17
17
  export { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./plugin/services.js";
@@ -1,7 +1,8 @@
1
1
  /** Menu commands: declaration, state sync (revisions hidden) and dispatch. */
2
2
  import { failure, Latest, quiet } from "./runtime.js";
3
3
  import { checkBindings } from "./bindings.js";
4
- import { MENU_IDS } from "../commands.js";
4
+ import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, MENU_IDS } from "../commands.js";
5
+ import { ImageHandle } from "./ui.js";
5
6
  import { isTextValue, textProblem } from "../i18n.js";
6
7
  const value = (item) => typeof item === "function" ? item() : item;
7
8
  /** Validate options and build the wire declaration. */
@@ -32,7 +33,7 @@ function declare(options) {
32
33
  if (!Number.isInteger(order) || Math.abs(order) > 1_000_000)
33
34
  throw failure("invalid", `command ${options.id}: order must be an integer within ±1000000`);
34
35
  const bindings = [...(options.bindings ?? [])];
35
- return {
36
+ const declaration = {
36
37
  title: title,
37
38
  path: path,
38
39
  order,
@@ -42,6 +43,32 @@ function declare(options) {
42
43
  focus: options.focus ?? null,
43
44
  repeat: options.repeat ?? false,
44
45
  };
46
+ // Only when set: players before 0.5.10 refuse the field.
47
+ if (options.button !== undefined)
48
+ declaration.button = button(options.id, options.button, path.length > 0);
49
+ return declaration;
50
+ }
51
+ /** `CommandOptions.button`, checked as the player checks it, in wire form. */
52
+ function button(id, value, inMenu) {
53
+ const fail = (problem) => failure("invalid", `command ${id}: ${problem}`);
54
+ if (value === null || typeof value !== "object")
55
+ throw fail("button is { icon, order }");
56
+ if (!inMenu)
57
+ throw fail("button needs a menu path: a bar button is also a menu item");
58
+ const { icon, order } = value;
59
+ if (!Number.isInteger(order) ||
60
+ order < 0 ||
61
+ order > BAR_BUTTON_LIMITS.maxOrder)
62
+ throw fail(`button order must be an integer 0–${BAR_BUTTON_LIMITS.maxOrder}`);
63
+ if (icon instanceof ImageHandle) {
64
+ if (icon.released)
65
+ throw fail(`button image ${icon.path} was released with the scope that created it`);
66
+ return { icon: icon.toJSON(), order };
67
+ }
68
+ if (typeof icon !== "string" ||
69
+ !BAR_BUTTON_ICONS.includes(icon))
70
+ throw fail(`unknown button icon ${JSON.stringify(icon)}: a name of @vestra/icons.slint (BAR_BUTTON_ICONS) or an image from plugin.ui.image`);
71
+ return { icon, order };
45
72
  }
46
73
  /** Unregistrations in flight per plugin: re-adding an id waits for its removal. */
47
74
  const removals = new WeakMap();
@@ -2,7 +2,7 @@ import type { Server } from "node:net";
2
2
  import { HostError } from "../wire.ts";
3
3
  import type { PackageManifest, PackageSource } from "./types.ts";
4
4
  import type { Json, LayoutSlot, LocalServices, PlaybackStats, Transport, WireSystemInfo } from "../wire.ts";
5
- import { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS } from "../commands.ts";
5
+ import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS } from "../commands.ts";
6
6
  import type { BuiltinCommand, BuiltinSubmenu } from "../commands.ts";
7
7
  import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.ts";
8
8
  import type { CapabilityInfo } from "../capabilities.ts";
@@ -22,6 +22,8 @@ import { LOCALES, LIST_SEPARATORS, TEXT_LIMITS } from "../i18n.ts";
22
22
  export { HostError };
23
23
  /** The player's command caps, which the test host enforces as well. */
24
24
  export { COMMAND_LIMITS, MENU_IDS };
25
+ /** The icons and limits of bottom-bar buttons, which the test host checks as well. */
26
+ export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS };
25
27
  /** The submenus of the player's built-in plugins, by key path. */
26
28
  export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS };
27
29
  export type { BuiltinCommand, BuiltinSubmenu };
@@ -199,6 +201,37 @@ export interface FakeCommand {
199
201
  */
200
202
  run(): Promise<void>;
201
203
  }
204
+ /**
205
+ * A command's bottom-bar button (`CommandOptions.button`) as the player shows
206
+ * it: an entry of {@link TestHost.barButtons}.
207
+ *
208
+ * @since 1.5.29
209
+ */
210
+ export interface FakeBarButton {
211
+ /** The command's local id. */
212
+ readonly command: string;
213
+ /** The icon name, or the image's path for a `plugin.ui.image` icon. */
214
+ readonly icon: string;
215
+ /** `true` when the icon is the plugin's image. */
216
+ readonly image: boolean;
217
+ /** The declared `order`. */
218
+ readonly order: number;
219
+ /** Clickable: the command is enabled and not busy. */
220
+ readonly enabled: boolean;
221
+ /** Drawn in the accent colour: the command is checked. */
222
+ readonly checked: boolean;
223
+ /**
224
+ * The tooltip without the key: the command's shown title (trailing "…"
225
+ * removed), then, while it cannot run, its reason on a second line.
226
+ */
227
+ readonly tooltip: string;
228
+ /**
229
+ * Click the button, then wait until the host is idle: runs the command
230
+ * as its menu item would. Rejects (with a {@link HostError}) while the
231
+ * button is disabled, and once the command is gone.
232
+ */
233
+ click(): Promise<void>;
234
+ }
202
235
  /**
203
236
  * A generated stream (`plugin.player.openStream`) as the test host sees it:
204
237
  * the fake player end of the frame connection. Assertion surface; see
@@ -1477,6 +1510,24 @@ export declare class TestHost implements Transport {
1477
1510
  * registered (never added, or disposed).
1478
1511
  */
1479
1512
  command(id: string): FakeCommand;
1513
+ /**
1514
+ * The plugin's bottom-bar buttons (`CommandOptions.button`) as the player
1515
+ * shows them: at most one, the first declared (`BAR_BUTTON_LIMITS`; a
1516
+ * later one stays a menu item only). Empty once the command is disposed.
1517
+ * Whether the bar places them is the bar's business: the list is what
1518
+ * the player hands to `BarButtons`.
1519
+ *
1520
+ * @example
1521
+ * ```ts
1522
+ * const [button] = host.barButtons;
1523
+ * assert.equal(button?.icon, "store");
1524
+ * await button?.click();
1525
+ * assert.equal(host.window("store").visible, true);
1526
+ * ```
1527
+ *
1528
+ * @since 1.5.29
1529
+ */
1530
+ get barButtons(): readonly FakeBarButton[];
1480
1531
  /**
1481
1532
  * The plugin's tool window with this id (by default the file name without
1482
1533
  * extension: `ui/picture.slint` → `picture`). Throws when it was never
@@ -66,7 +66,7 @@ import { extname, isAbsolute, join } from "node:path";
66
66
  import { fileURLToPath } from "node:url";
67
67
  import { HostError } from "../wire.js";
68
68
  import { localSocketPath } from "./local.js";
69
- import { BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "../commands.js";
69
+ import { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS, BUILTIN_COMMANDS, BUILTIN_SUBMENUS, COMMAND_LIMITS, MENU_IDS, } from "../commands.js";
70
70
  import { CAPABILITY_INFO, MESSAGE_LIMIT } from "../capabilities.js";
71
71
  import { OSD_LIMITS } from "./osd.js";
72
72
  import { SECRET_LIMITS, SESSION_STORAGE_LIMITS } from "./services.js";
@@ -83,6 +83,8 @@ import { Catalog, format as formatTemplate, HOST_GROUPS, HOST_ZH_HANS, LOCALES,
83
83
  export { HostError };
84
84
  /** The player's command caps, which the test host enforces as well. */
85
85
  export { COMMAND_LIMITS, MENU_IDS };
86
+ /** The icons and limits of bottom-bar buttons, which the test host checks as well. */
87
+ export { BAR_BUTTON_ICONS, BAR_BUTTON_LIMITS };
86
88
  /** The submenus of the player's built-in plugins, by key path. */
87
89
  export { BUILTIN_COMMANDS, BUILTIN_SUBMENUS };
88
90
  /** The manifest capabilities: what each opens and how the store shows it. */
@@ -2095,6 +2097,53 @@ export class TestHost {
2095
2097
  throw new Error(`command ${id} is not registered`);
2096
2098
  return command;
2097
2099
  }
2100
+ /**
2101
+ * The plugin's bottom-bar buttons (`CommandOptions.button`) as the player
2102
+ * shows them: at most one, the first declared (`BAR_BUTTON_LIMITS`; a
2103
+ * later one stays a menu item only). Empty once the command is disposed.
2104
+ * Whether the bar places them is the bar's business: the list is what
2105
+ * the player hands to `BarButtons`.
2106
+ *
2107
+ * @example
2108
+ * ```ts
2109
+ * const [button] = host.barButtons;
2110
+ * assert.equal(button?.icon, "store");
2111
+ * await button?.click();
2112
+ * assert.equal(host.window("store").visible, true);
2113
+ * ```
2114
+ *
2115
+ * @since 1.5.29
2116
+ */
2117
+ get barButtons() {
2118
+ const host = this;
2119
+ return [...this.commands]
2120
+ .filter(([, command]) => command.declaration.button !== undefined)
2121
+ .slice(0, BAR_BUTTON_LIMITS.perPlugin)
2122
+ .map(([id, command]) => {
2123
+ const button = command.declaration.button;
2124
+ const image = typeof button.icon !== "string";
2125
+ const reason = command.display.reason;
2126
+ const title = command.display.title.replace(/(…|\.\.\.)\s*$/, "");
2127
+ const enabled = command.state.enabled && !command.state.busy;
2128
+ return Object.freeze({
2129
+ command: id,
2130
+ icon: image
2131
+ ? String(button.icon.$vestraImage)
2132
+ : button.icon,
2133
+ image,
2134
+ order: button.order,
2135
+ enabled,
2136
+ checked: command.state.checked === true,
2137
+ tooltip: !enabled && reason ? `${title}\n${reason}` : title,
2138
+ async click() {
2139
+ const current = host.commands.get(id);
2140
+ if (current !== command)
2141
+ throw new HostError("missing", `command ${id} is gone`);
2142
+ await current.run();
2143
+ },
2144
+ });
2145
+ });
2146
+ }
2098
2147
  /**
2099
2148
  * The plugin's tool window with this id (by default the file name without
2100
2149
  * extension: `ui/picture.slint` → `picture`). Throws when it was never
@@ -2501,6 +2550,23 @@ export class TestHost {
2501
2550
  throw new HostError("limit", "command registry limit reached");
2502
2551
  const host = this;
2503
2552
  const declaration = p.declaration;
2553
+ // Player 0.5.10: a bar button needs a path, an order 0–999 and a
2554
+ // known icon or an image of the plugin.
2555
+ if (declaration.button !== undefined) {
2556
+ const { icon, order } = declaration.button;
2557
+ if (path.length === 0)
2558
+ throw new HostError("invalid", "button needs a menu path");
2559
+ if (!Number.isInteger(order) ||
2560
+ order < 0 ||
2561
+ order > BAR_BUTTON_LIMITS.maxOrder)
2562
+ throw new HostError("invalid", `button order must be an integer 0–${BAR_BUTTON_LIMITS.maxOrder}`);
2563
+ if (typeof icon === "string") {
2564
+ if (!BAR_BUTTON_ICONS.includes(icon))
2565
+ throw new HostError("invalid", `unknown button icon "${icon}"`);
2566
+ }
2567
+ else if (this.imagePath(icon) === null)
2568
+ throw new HostError("invalid", "button icon is an icon name or an image from plugin.ui.image");
2569
+ }
2504
2570
  // Like the player: the menu texts and the reason are checked (and
2505
2571
  // shown) when they arrive.
2506
2572
  for (const segment of [declaration.title, ...(declaration.path ?? [])])
@@ -386,6 +386,77 @@ export type Bindable<T> = T | (() => T);
386
386
  * `MENU_IDS` in `@vestra-player/sdk/testing`.
387
387
  */
388
388
  export type MenuId = "open" | "playback" | "subtitles" | "video" | "audio" | "screen" | "settings";
389
+ /**
390
+ * An icon of the player's V-Line set (`@vestra/icons.slint`), by name: what
391
+ * a bottom-bar button may show. The list is `BAR_BUTTON_ICONS` in
392
+ * `@vestra-player/sdk/testing`.
393
+ *
394
+ * @since 1.5.29
395
+ */
396
+ export type BarButtonIcon = "play" | "pause" | "stop" | "back" | "forward" | "open" | "volume" | "mute" | "playlist" | "detach" | "attach" | "close" | "failed" | "move-top" | "move-up" | "move-down" | "move-bottom" | "search" | "chapter" | "bookmark" | "check-task" | "check-box" | "menu-check" | "submenu" | "scroll-up" | "scroll-down" | "caret-down" | "sort-asc" | "sort-desc" | "tag-remove" | "preview-play" | "add" | "remove" | "sort" | "fullscreen" | "exit-fullscreen" | "store" | "ai";
397
+ /**
398
+ * A command's button on the bottom bar: {@link CommandOptions.button}.
399
+ *
400
+ * @remarks
401
+ * **Where it shows.** The bottom bar places the player's strip of command
402
+ * buttons, `BarButtons` from `@vestra/bar-buttons.slint`, at its right end;
403
+ * the player fills it with the buttons of every plugin, left to right by
404
+ * `order`, and its own sidebar toggle rightmost. When the window is narrow
405
+ * the strip loses cells from its outer (left) end inward: the lowest order
406
+ * goes first, the sidebar toggle last. A
407
+ * bottom bar that does not place `BarButtons` shows no buttons: the
408
+ * command's menu entry stays, and the player logs the buttons it cannot
409
+ * show and whose bar hides them. The built-in bar (`vestra.playback`) places
410
+ * it; see
411
+ * [Writing a bottom bar](../../docs/guidelines/interface.md#writing-a-bottom-bar).
412
+ *
413
+ * **What it shows.** The icon, the command's title (translated, a trailing
414
+ * "…" removed) and its first key as the tooltip ("Plugin Store (F9)"),
415
+ * `enabled`, `busy` and `reason` as the menu item has them: a disabled or
416
+ * busy command's button is drawn disabled, its tooltip adds the reason, and
417
+ * a click runs nothing. A command with `checked` shows its icon in the
418
+ * accent colour while checked. A click runs the command exactly like a
419
+ * click on its menu item.
420
+ *
421
+ * **Limits.** One button per plugin and eight in the player
422
+ * (`BAR_BUTTON_LIMITS`): a button past them is left out (its command and
423
+ * menu entry stay) and the player logs it with `limit`. The button goes
424
+ * with its command: disposing the command, or disabling, stopping or
425
+ * uninstalling the plugin, removes it.
426
+ *
427
+ * @example
428
+ * ```ts
429
+ * const store = plugin.windows.create("ui/store.slint");
430
+ * plugin.commands.add({
431
+ * id: "open",
432
+ * path: ["Plugin Store…"],
433
+ * button: { icon: "store", order: 200 },
434
+ * run: () => store.show(),
435
+ * });
436
+ * ```
437
+ *
438
+ * @player 0.5.10; older players show the menu entry without a button
439
+ * (declare `"minPlayerVersion": "0.5.10"` when the button matters).
440
+ * @since 1.5.29
441
+ */
442
+ export interface CommandButton {
443
+ /**
444
+ * A name of the player's icon set ({@link BarButtonIcon}), or an image
445
+ * made with {@link Ui.image} (a PNG or JPEG in your package or data
446
+ * directory), drawn in the 14×12 icon box, aspect kept, coloured like the
447
+ * player's line icons: only its alpha counts, so draw it as a dark shape
448
+ * on a transparent background. An unknown name fails with `invalid`.
449
+ */
450
+ icon: BarButtonIcon | ImageRef;
451
+ /**
452
+ * Place in the strip, left to right ascending: an integer 0–999. It is
453
+ * position only: a lower order sits further left, and in a narrow window
454
+ * (or past the limit of eight) it is the first to go. Equal orders sort
455
+ * by plugin id, then declaration order. The player's own buttons use AI
456
+ * 100, Plugin Store 200.
457
+ */
458
+ order: number;
459
+ }
389
460
  /**
390
461
  * The declaration passed to {@link Commands.add}.
391
462
  *
@@ -500,6 +571,16 @@ export interface CommandOptions {
500
571
  focus?: "panel";
501
572
  /** Fire again on keyboard auto-repeat while the key is held. Default false. */
502
573
  repeat?: boolean;
574
+ /**
575
+ * A button for this command on the bottom bar (SDK 1.5.29, player 0.5.10):
576
+ * an icon and a place, `{ icon: "store", order: 200 }`. Needs a `path`: the
577
+ * menu entry is where the command stays reachable when the bar shows no
578
+ * button (a bar without `BarButtons`, a narrow window, a limit). See
579
+ * {@link CommandButton} for where it shows, what it shows and the limits.
580
+ * Fails with `invalid` for an unknown icon name, an order outside 0–999 or
581
+ * a button without a path.
582
+ */
583
+ button?: CommandButton;
503
584
  /** Whether the command can run (default true). */
504
585
  enabled?: Bindable<boolean>;
505
586
  /** Check mark (or radio dot with `radio`); `null` (default) draws none. */
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.28: full reference
1
+ # @vestra-player/sdk 1.5.29: 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
 
@@ -498,11 +498,25 @@ The user can rebind any command, the player's own included. Those **overrides**
498
498
  - `list()` returns every declared command with its default and current inputs, which of them another command shadows, and the overrides whose command does not exist right now (its plugin is stopped); those are kept and apply again when it returns.
499
499
  - To find a command by its place in the menu, compare BindingInfo.key (the English menu keys, the same in every language), not `path`, which is the translated text.
500
500
 
501
+ #### Bottom-bar buttons
502
+
503
+ A command can also be a button on the bottom bar: give it `button: { icon, order }` (CommandButton, player 0.5.10) besides its menu `path`. The player shows the buttons of all plugins in one strip at the bar's right end, left to right by `order`, its own **Toggle Sidebar** rightmost; in a narrow window the lowest order (leftmost) goes first and the toggle last; a click runs the command like its menu item, the tooltip is the item's title and first key, and a disabled or busy command's button is drawn disabled with the reason in its tooltip. One button per plugin and eight in the player (`BAR_BUTTON_LIMITS`). The strip lives in the bottom bar's own layout (`BarButtons`, [Writing a bottom bar): a bar that does not place it shows no buttons, which is why a button always has a menu entry too.
504
+
505
+ ```ts
506
+ const store = plugin.windows.create("ui/store.slint");
507
+ plugin.commands.add({
508
+ id: "open",
509
+ path: ["Plugin Store…"],
510
+ button: { icon: "store", order: 200 },
511
+ run: () => store.show(),
512
+ });
513
+ ```
514
+
501
515
  #### The player's own commands
502
516
 
503
- The player registers some commands itself, with the owner `host`: **Toggle Fullscreen**, **Leave Fullscreen**, **Quit** and the core playback actions (play/pause, stop, previous/next, frame step, relative seek, speed, volume, mute). They exist whether or not any plugin runs, and the user rebinds them like any other command.
517
+ The player registers some commands itself, with the owner `host`: **Toggle Fullscreen**, **Leave Fullscreen**, **Quit**, **Toggle Sidebar** (the bottom bar's sidebar button, player 0.5.10) and the core playback actions (play/pause, stop, previous/next, frame step, relative seek, speed, volume, mute). They exist whether or not any plugin runs, and the user rebinds them like any other command.
504
518
 
505
- A plugin button that does what a core action does should run that action with plugin.host.run (capability `commands`): it applies the same checks and shows the same on-screen text as the menu item and the key, so a bottom bar's play button and the Space key behave identically. The action ids, their menu places and default keys are in [Reference: commands; what they do to playback is in Playback.
519
+ A plugin button that does what a core action does should run that action with plugin.host.run (capability `commands`): it applies the same checks and shows the same on-screen text as the menu item and the key, so a bottom bar's play button and the Space key behave identically. The action ids, their menu places and default keys are in Reference: commands; what they do to playback is in Playback.
506
520
 
507
521
  ## Concept: Playback state, controls and sources
508
522
 
@@ -779,7 +793,7 @@ It assumes you have written a first plugin (Getting started). For every method's
779
793
  Two rules of thumb:
780
794
 
781
795
  - **Prefer the smallest surface that works.** An OSD message before a dialog, a settings page before a tool window. Every window the user has to close is a cost.
782
- - **Panels are shared space.** There is one sidebar, one bottom bar and one backdrop for all plugins. The sidebar holds several panels as tabs (the player draws the tab strip while there are two or more; showing, hiding and detaching apply to all of them). The bottom bar and the backdrop hold one: while one plugin's panel is there, another plugin's mount rejects with `conflict`, which does not fail its activation. Mount a panel only when the plugin's main purpose is to live there.
796
+ - **Panels are shared space.** There is one sidebar, one bottom bar and one backdrop for all plugins. The sidebar holds several panels as tabs (the player draws the tab strip while there are two or more; showing, hiding and detaching apply to all of them). The bottom bar and the backdrop hold one: while one plugin's panel is there, another plugin's mount rejects with `conflict`, which does not fail its activation. Mount a panel only when the plugin's main purpose is to live there. A command that needs a one-click entry point does not need a panel: give it a bottom-bar button (`button` on commands.add), which the player shows in the strip of command buttons every bottom bar places (Writing a bottom bar).
783
797
 
784
798
  #### Life cycles
785
799
 
@@ -2081,6 +2095,29 @@ The player's picture, audio and subtitle tools share one layout; a window of the
2081
2095
  - **Commands of a panel go into its right-click menu**, made of the plugin's commands (panel.showMenu); see Menus.
2082
2096
  - **The bottom bar is icons only**: no text buttons, no source path, no status text (the source name is in the window title, status is an OSD message).
2083
2097
 
2098
+ ##### Writing a bottom bar
2099
+
2100
+ The bottom bar holds one panel, and every plugin's command buttons (CommandOptions.button) and the player's sidebar toggle show inside it, in a strip the player fills. A bar that replaces the built-in one (`vestra.playback`) must place that strip, or those buttons show nowhere while it is mounted (their menu entries stay):
2101
+
2102
+ - **Place `BarButtons`** from `@vestra/bar-buttons.slint` at the right end of the button row, and give it `room`: the width left after your own controls. It shows as many 41×40 cells as fit and loses them from its outer (left) end inward: of the plugins' buttons the lowest order goes first, and the sidebar toggle goes last. The strip is as wide as the cells it shows, so lay your controls out against its `width`. Do not put it under an `if`: it must exist for the player to see it placed. `first-shown` (out) is the index of the leftmost plugin button shown, if your layout needs it.
2103
+ - **Decide what goes first when the window narrows.** The built-in bar gives the strip only the room its own buttons and the time leave, so the strip shrinks first, then its Fullscreen button hides, and the time is never cut.
2104
+ - **Do not draw a sidebar toggle of your own**: the strip has the player's (**Toggle Sidebar**, `host/sidebar`), with its state.
2105
+ - `vestra-plugin check` warns when a plugin mounts a bottom bar whose Slint file does not import `BarButtons` from `@vestra/bar-buttons.slint`, itself or through the plugin's own Slint files it imports, and the player logs the buttons a mounted bar hides.
2106
+
2107
+ ```slint
2108
+ import { BarButtons } from "@vestra/bar-buttons.slint";
2109
+
2110
+ export component MyBar inherits Rectangle {
2111
+ height: 40px;
2112
+ // Your controls on the left, 3 × 41px wide …
2113
+ strip := BarButtons {
2114
+ x: root.width - self.width - 4px;
2115
+ y: 0px;
2116
+ room: max(0px, root.width - 3 * 41px - 8px);
2117
+ }
2118
+ }
2119
+ ```
2120
+
2084
2121
  #### Text
2085
2122
 
2086
2123
  - **Every visible text is English source text** that the player translates (Translate a plugin). Write short, plain sentences; one sentence per status or error.
@@ -2253,6 +2290,7 @@ The table lists the API whose documentation names a player version, newest last:
2253
2290
  | 0.5.2 | HostAction |
2254
2291
  | 0.5.3 | Scrub.seek |
2255
2292
  | 0.5.5 | SessionStorage |
2293
+ | 0.5.10 | CommandButton, CommandOptions.button |
2256
2294
 
2257
2295
  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).
2258
2296
 
@@ -2330,6 +2368,16 @@ A command added past a limit fails with code `limit` (its `ready` rejects; durin
2330
2368
  | `COMMAND_LIMITS.perPlugin` | 160 | Commands one plugin instance may have at the same time. |
2331
2369
  | `COMMAND_LIMITS.total` | 1024 | Commands of every plugin plus the player's own, together. |
2332
2370
 
2371
+ #### Bottom-bar buttons
2372
+
2373
+ A command's button (CommandOptions.button, player 0.5.10) past a limit is left out, not refused: the command registers and its menu entry stays, and the player logs the button with `limit`. Each plugin keeps its first declared button; of those, the player keeps the eight highest orders, so the lowest go first, as in a narrow strip (equal orders: the smaller plugin id, then the earlier declaration, goes first). The player's own sidebar toggle is extra.
2374
+
2375
+ | Limit | Value | Meaning |
2376
+ | --- | --- | --- |
2377
+ | `BAR_BUTTON_LIMITS.perPlugin` | 1 | Buttons one plugin shows. |
2378
+ | `BAR_BUTTON_LIMITS.total` | 8 | Buttons the player shows, every plugin together, besides its own sidebar toggle. |
2379
+ | `BAR_BUTTON_LIMITS.maxOrder` | 999 | The highest `order`. |
2380
+
2333
2381
  #### Property values
2334
2382
 
2335
2383
  One value written to a window, panel or settings page property (View.set, View.bind or an initial property). A value over a limit is refused whole with code `limit`; the property keeps what it showed.
@@ -2784,6 +2832,7 @@ Every command the player (`host`) and its built-in plugins register when the pla
2784
2832
  | About Vestra Player… | `F1` | `vestra.info/info.about` |
2785
2833
  | Quit | `Cmd+Q` | `host/quit` |
2786
2834
  | Leave Fullscreen (not in the menu) | `Escape` | `host/exit-fullscreen` |
2835
+ | Toggle Sidebar (not in the menu) | | `host/sidebar` |
2787
2836
  | Brief Info (not in the menu) | `Shift+Tab` | `vestra.info/info.brief` |
2788
2837
  | Play (not in the menu) | `Enter`, `P` (panel focus) | `vestra.playlist/playlist.play` |
2789
2838
  | Add Files… (not in the menu) | `Ctrl+I` (panel focus) | `vestra.playlist/playlist.list-add` |
@@ -2817,6 +2866,23 @@ Every command the player (`host`) and its built-in plugins register when the pla
2817
2866
  | Close List (not in the menu) | `F6` (panel focus) | `vestra.playlist/playlist.close` |
2818
2867
  | Playlist Settings… (not in the menu) | `F5` (panel focus) | `vestra.playlist/playlist.settings` |
2819
2868
 
2869
+ #### Bottom-bar buttons
2870
+
2871
+ `button: { icon, order }` on commands.add (CommandButton, SDK 1.5.29, player 0.5.10) gives a command a button in the bottom bar's strip of command buttons (`BarButtons`, Writing a bottom bar).
2872
+
2873
+ | Field | Rule |
2874
+ | --- | --- |
2875
+ | `icon` | A name of the player's icon set (the names in Theme: Icons, `BAR_BUTTON_ICONS`), or an image from plugin.ui.image drawn in the 14×12 icon box in the icon colour (only its alpha counts). Unknown name: `invalid`. |
2876
+ | `order` | Integer 0–999, left to right ascending; equal orders by plugin id, then declaration order. The built-in plugins use 100 (Vestra Player AI) and 200 (Plugin Store). |
2877
+ | `path` | Required: a button without a menu path is `invalid`. |
2878
+
2879
+ - **Shown**: the icon; the tooltip is the command's shown title without a trailing "…" and its first active key ("Plugin Store (F9)"), plus the reason on a second line while the command is disabled or busy. Disabled or busy: drawn disabled, a click runs nothing. `checked: true`: the icon in the accent colour. Accessibility: a button named by the title, with its enabled and checked state and a default action.
2880
+ - **Click**: runs the command exactly as a click on its menu item does.
2881
+ - **Order and room**: left to right by `order`, the player's **Toggle Sidebar** rightmost; in a narrow window the strip loses cells from its outer (left) end inward: the lowest order goes first, the sidebar toggle last. `order` is position only: low is further left and goes first.
2882
+ - **Limits** (Limits): one button per plugin (its first declared), eight in the player (the highest orders; the lowest go first, ties: the smaller plugin id, then the earlier declaration). A button past them is left out and logged with `limit`; the command and its menu entry stay.
2883
+ - **Life cycle**: the button belongs to its command. Disposing the command, or disabling, stopping or uninstalling the plugin, removes it.
2884
+ - **Older players** (before 0.5.10) build the declaration with their own copy of the SDK, which does not send `button`: the command and its menu entry work, without a button. Declare `"minPlayerVersion": "0.5.10"` when the button matters.
2885
+
2820
2886
  #### Binding syntax
2821
2887
 
2822
2888
  ```text
@@ -2867,7 +2933,7 @@ The player's own playback commands, which plugin.host.run runs by id (capability
2867
2933
  | `mute` | Audio ▸ Mute | `M` |
2868
2934
  | `fullscreen` | Toggle Fullscreen | `Enter`, `Alt+Enter`, `F11` |
2869
2935
 
2870
- The player also owns **Toggle Fullscreen** (`host/fullscreen`), **Leave Fullscreen** (`host/exit-fullscreen`, `Escape`, only in fullscreen) and **Quit** (`host/quit`); their keys are in Built-in commands.
2936
+ The player also owns **Toggle Fullscreen** (`host/fullscreen`), **Leave Fullscreen** (`host/exit-fullscreen`, `Escape`, only in fullscreen) and **Quit** (`host/quit`), and **Toggle Sidebar** (`host/sidebar`, player 0.5.10: the bottom bar's sidebar button, outside the menu, no default key); their keys are in Built-in commands.
2871
2937
 
2872
2938
  #### Keys the system takes first
2873
2939
 
@@ -3042,6 +3108,8 @@ Icon { name: "close"; width: 20px; height: 20px; } // scaled
3042
3108
  | `sort` | 10 × 10 | 1.5px |
3043
3109
  | `fullscreen` | 14 × 12 | 1.5px |
3044
3110
  | `exit-fullscreen` | 14 × 12 | 1.5px |
3111
+ | `store` | 14 × 12 | 1.5px |
3112
+ | `ai` | 14 × 12 | 1.5px |
3045
3113
 
3046
3114
  #### Widgets
3047
3115
 
@@ -3123,6 +3191,19 @@ Struct **`MarkdownRow`**: One table row (cells are plain text).
3123
3191
 
3124
3192
  Struct **`MarkdownBlock`**: One displayed block. Plugins only pass these through; the fields are filled by the player.
3125
3193
 
3194
+ #### Bar buttons
3195
+
3196
+ `@vestra/bar-buttons.slint` (player 0.5.10) is the strip of command buttons a bottom bar places at its right end: the buttons plugins give their commands (CommandOptions.button) and the player's sidebar toggle. The player fills and runs it; the bar only places it and gives it `room`, the width it can spare. See Writing a bottom bar.
3197
+
3198
+ **`BarButtons`**: The strip: place it at the bar's right end and give it the width you can spare; it is as wide as the cells it shows (read `width` to lay out the rest).
3199
+
3200
+ | Member | Kind | Default | Note |
3201
+ | --- | --- | --- | --- |
3202
+ | `room` | in `length` | | The width the bar can give the strip; the cells that fit show. |
3203
+ | `first-shown` | out `int` | `BarButtonsHost.buttons.length - root.count` | The index (in the player's list, ascending order) of the leftmost plugin button shown; the ones before it are hidden for want of room. |
3204
+
3205
+ Struct **`BarButtonItem`**: One button as the player shows it.
3206
+
3126
3207
  ## Reference: Command line
3127
3208
 
3128
3209
  `vestra-plugin` comes with `@vestra-player/sdk` and works in any project that depends on it (`npx vestra-plugin …`, or an npm script). A plugin directory is a directory with `manifest.json`; the default is the current directory, and several can be given at once.
@@ -3494,6 +3575,35 @@ Playback, Subtitles, Video, Audio, Screen, Settings. The list is
3494
3575
  type MenuId = "open" | "playback" | "subtitles" | "video" | "audio" | "screen" | "settings";
3495
3576
  ```
3496
3577
 
3578
+ ### BarButtonIcon
3579
+
3580
+ An icon of the player's V-Line set (`@vestra/icons.slint`), by name: what
3581
+ a bottom-bar button may show. The list is `BAR_BUTTON_ICONS` in
3582
+ `@vestra-player/sdk/testing`.
3583
+
3584
+ ```ts
3585
+ type BarButtonIcon = "play" | "pause" | "stop" | "back" | "forward" | "open" | "volume" | "mute" | "playlist" | "detach" | "attach" | "close" | "failed" | "move-top" | "move-up" | "move-down" | "move-bottom" | "search" | "chapter" | "bookmark" | "check-task" | "check-box" | "menu-check" | "submenu" | "scroll-up" | "scroll-down" | "caret-down" | "sort-asc" | "sort-desc" | "tag-remove" | "preview-play" | "add" | "remove" | "sort" | "fullscreen" | "exit-fullscreen" | "store" | "ai";
3586
+ ```
3587
+
3588
+ ### CommandButton
3589
+
3590
+ A command's button on the bottom bar: CommandOptions.button.
3591
+
3592
+ - @player 0.5.10; older players show the menu entry without a button (declare `"minPlayerVersion": "0.5.10"` when the button matters).
3593
+
3594
+ ```ts
3595
+ interface CommandButton {
3596
+ /**
3597
+ * A name of the player's icon set (BarButtonIcon), or an image made with Ui.image (a PNG or JPEG in your package or data directory), drawn in the 14×12 icon box, aspect kept, coloured like the player's line icons: only its alpha counts, so draw it as a dark shape on a transparent background. An unknown name fails with `invalid`.
3598
+ */
3599
+ icon: BarButtonIcon | ImageRef;
3600
+ /**
3601
+ * Place in the strip, left to right ascending: an integer 0–999. It is position only: a lower order sits further left, and in a narrow window (or past the limit of eight) it is the first to go. Equal orders sort by plugin id, then declaration order. The player's own buttons use AI 100, Plugin Store 200.
3602
+ */
3603
+ order: number;
3604
+ }
3605
+ ```
3606
+
3497
3607
  ### CommandOptions
3498
3608
 
3499
3609
  The declaration passed to Commands.add.
@@ -3592,6 +3702,10 @@ interface CommandOptions {
3592
3702
  focus?: "panel";
3593
3703
  /** Fire again on keyboard auto-repeat while the key is held. Default false. */
3594
3704
  repeat?: boolean;
3705
+ /**
3706
+ * A button for this command on the bottom bar (SDK 1.5.29, player 0.5.10): an icon and a place, `{ icon: "store", order: 200 }`. Needs a `path`: the menu entry is where the command stays reachable when the bar shows no button (a bar without `BarButtons`, a narrow window, a limit). See CommandButton for where it shows, what it shows and the limits. Fails with `invalid` for an unknown icon name, an order outside 0–999 or a button without a path.
3707
+ */
3708
+ button?: CommandButton;
3595
3709
  /** Whether the command can run (default true). */
3596
3710
  enabled?: Bindable<boolean>;
3597
3711
  /** Check mark (or radio dot with `radio`); `null` (default) draws none. */
@@ -7011,6 +7125,34 @@ interface FakeCommand {
7011
7125
  }
7012
7126
  ```
7013
7127
 
7128
+ ### FakeBarButton
7129
+
7130
+ A command's bottom-bar button (`CommandOptions.button`) as the player shows
7131
+ it: an entry of TestHost.barButtons.
7132
+
7133
+ ```ts
7134
+ interface FakeBarButton {
7135
+ /** The command's local id. */
7136
+ readonly command: string;
7137
+ /** The icon name, or the image's path for a `plugin.ui.image` icon. */
7138
+ readonly icon: string;
7139
+ /** `true` when the icon is the plugin's image. */
7140
+ readonly image: boolean;
7141
+ /** The declared `order`. */
7142
+ readonly order: number;
7143
+ /** Clickable: the command is enabled and not busy. */
7144
+ readonly enabled: boolean;
7145
+ /** Drawn in the accent colour: the command is checked. */
7146
+ readonly checked: boolean;
7147
+ /** The tooltip without the key: the command's shown title (trailing "…" removed), then, while it cannot run, its reason on a second line. */
7148
+ readonly tooltip: string;
7149
+ /**
7150
+ * Click the button, then wait until the host is idle: runs the command as its menu item would. Rejects (with a HostError) while the button is disabled, and once the command is gone.
7151
+ */
7152
+ click(): Promise<void>;
7153
+ }
7154
+ ```
7155
+
7014
7156
  ### FakeProducer
7015
7157
 
7016
7158
  A generated stream (`plugin.player.openStream`) as the test host sees it:
@@ -7810,6 +7952,10 @@ class TestHost implements Transport {
7810
7952
  step(direction: "next" | "previous"): Promise<unknown>;
7811
7953
  /** The plugin's command with this local id. Throws when it is not registered (never added, or disposed). */
7812
7954
  command(id: string): FakeCommand;
7955
+ /**
7956
+ * The plugin's bottom-bar buttons (`CommandOptions.button`) as the player shows them: at most one, the first declared (`BAR_BUTTON_LIMITS`; a later one stays a menu item only). Empty once the command is disposed. Whether the bar places them is the bar's business: the list is what the player hands to `BarButtons`.
7957
+ */
7958
+ get barButtons(): readonly FakeBarButton[];
7813
7959
  /**
7814
7960
  * The plugin's tool window with this id (by default the file name without extension: `ui/picture.slint` → `picture`). Throws when it was never opened with `show()`.
7815
7961
  */
@@ -7991,6 +8137,39 @@ agrees with the player's.
7991
8137
  const MENU_IDS: readonly ["open", "playback", "subtitles", "video", "audio", "screen", "settings"];
7992
8138
  ```
7993
8139
 
8140
+ ### BAR_BUTTON_ICONS
8141
+
8142
+ The icons a bottom-bar button may name (`CommandOptions.button.icon`):
8143
+ every icon of the player's V-Line set, `@vestra/icons.slint` (draw one
8144
+ in your own Slint with `Icon { name: "store"; }`). The player's tests check
8145
+ that this list is its icon set.
8146
+
8147
+ ```ts
8148
+ const BAR_BUTTON_ICONS: readonly ["play", "pause", "stop", "back", "forward", "open", "volume", "mute", "playlist", "detach", "attach", "close", "failed", "move-top", "move-up", "move-down", "move-bottom", "search", "chapter", "bookmark", "check-task", "check-box", "menu-check", "submenu", "scroll-up", "scroll-down", "caret-down", "sort-asc", "sort-desc", "tag-remove", "preview-play", "add", "remove", "sort", "fullscreen", "exit-fullscreen", "store", "ai"];
8149
+ ```
8150
+
8151
+ ### BAR_BUTTON_LIMITS
8152
+
8153
+ The player's limits on bottom-bar buttons (`CommandOptions.button`), the
8154
+ same values as the player's (its tests check that they agree). A button
8155
+ past `perPlugin` or `total` is left out: its command still registers and
8156
+ its menu entry stays, and the player logs the button it left out
8157
+ (`limit`). Which ones stay: each plugin's first declared button, then the
8158
+ `total` highest by order, plugin id and declaration order: as in a narrow
8159
+ strip, the lowest order goes first (equal orders: the smaller plugin id,
8160
+ then the earlier declaration, goes first).
8161
+
8162
+ ```ts
8163
+ const BAR_BUTTON_LIMITS: {
8164
+ /** Buttons one plugin shows (1). */
8165
+ readonly perPlugin: 1;
8166
+ /** Buttons the player shows, every plugin together, besides its own sidebar toggle (8). */
8167
+ readonly total: 8;
8168
+ /** The highest `order` (999; the lowest is 0). */
8169
+ readonly maxOrder: 999;
8170
+ };
8171
+ ```
8172
+
7994
8173
  ### BuiltinSubmenu
7995
8174
 
7996
8175
  A submenu that one of the player's built-in plugins declares: an entry of
@@ -8683,6 +8862,13 @@ type CommandDeclaration = {
8683
8862
  focus: "panel" | null;
8684
8863
  /** Fire again on keyboard auto-repeat while held. */
8685
8864
  repeat: boolean;
8865
+ /**
8866
+ * A bottom-bar button (player 0.5.10): an icon name, or an image in the `plugin.ui.image` wire form. Sent only when set: older players refuse unknown declaration fields.
8867
+ */
8868
+ button?: {
8869
+ icon: string | Json;
8870
+ order: number;
8871
+ };
8686
8872
  };
8687
8873
  ```
8688
8874
 
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.28: 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; it runs in its own Node.js process. Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n> are line numbers in llms-full.txt, next to this file: every page, declaration and example.
3
+ > @vestra-player/sdk 1.5.29: 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; it runs in its own Node.js process. Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n> are line numbers in llms-full.txt, next to this file: every page, declaration and example.
4
4
 
5
5
  **Rules**
6
6
 
@@ -33,7 +33,7 @@ my-plugin/
33
33
  }
34
34
  ```
35
35
 
36
- Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2194–2260.
36
+ Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2231–2298.
37
37
 
38
38
  **main.ts**
39
39
 
@@ -76,9 +76,9 @@ export default async function activate(plugin: Plugin) {
76
76
  - `ui.settings`: Settings.page/show
77
77
  - `ui.theme`: Appearance.set/setLanguage
78
78
  - `secrets`: Secrets.get/set/delete
79
- - No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme and packages.list. Details: L2261–2319.
79
+ - No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme and packages.list. Details: L2299–2357.
80
80
 
81
- **Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`; `host` is a TestHost (an in-memory player): `host.setSource(…)`, `host.command(id).run()`, `host.press(key)`, `await host.settle()`, then assert on `host.osd`, `host.window(id).props`… Guide: L1658–1846; TestHost: L7056.
81
+ **Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`; `host` is a TestHost (an in-memory player): `host.setSource(…)`, `host.command(id).run()`, `host.press(key)`, `await host.settle()`, then assert on `host.osd`, `host.window(id).props`… Guide: L1672–1860; TestHost: L7198.
82
82
 
83
83
  **Command line**
84
84
 
@@ -106,39 +106,39 @@ export default async function activate(plugin: Plugin) {
106
106
  - `limit`: A player-wide limit was reached, a property value is over the limits of PropValue
107
107
  - `reserved`: The id is `vestra` or starts with `vestra.`
108
108
  - `versionMismatch`: The package needs a newer player or another `apiVersion`
109
- - Others: PluginError, llms-full.txt L3226.
109
+ - Others: PluginError, llms-full.txt L3307.
110
110
 
111
111
  ## Reference
112
112
 
113
113
  - [llms-full.txt](https://vestrasys.github.io/vestra-player-sdk/llms-full.txt), also node_modules/@vestra-player/sdk/llms-full.txt; the pages are on the [site](https://vestrasys.github.io/vestra-player-sdk/).
114
114
  - Tutorials: Getting started L9–237
115
- - Concepts: How a plugin runs L238–408; Commands, menus and key bindings L409–506; Playback state, controls and sources L507–652; Video and audio processing L653–731; Windows, panels, settings pages, dialogs and the OSD L732–1020
116
- - How-to: Keep a slider responsive while dragging L1021–1195; Remember a window's state between runs L1196–1325; Translate a plugin L1326–1509; Migrate a plugin to English source text L1510–1657; Test a plugin L1658–1846; Publish a plugin L1847–1935; Migrating off package rollback (1.5.16, removed in 1.5.17) L1936–2011
117
- - Guidelines: Interface guidelines L2012–2096; Menu guidelines L2097–2193
118
- - Reference: Manifest L2194–2260; Capabilities L2261–2319; Limits L2320–2424; Commands, menus and bindings L2425–2889; Platforms L2890–2918; Theme L2919–3125; Command line L3126–3183
119
- - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3184–6891
120
- - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L6892–8297
115
+ - Concepts: How a plugin runs L238–408; Commands, menus and key bindings L409–520; Playback state, controls and sources L521–666; Video and audio processing L667–745; Windows, panels, settings pages, dialogs and the OSD L746–1034
116
+ - How-to: Keep a slider responsive while dragging L1035–1209; Remember a window's state between runs L1210–1339; Translate a plugin L1340–1523; Migrate a plugin to English source text L1524–1671; Test a plugin L1672–1860; Publish a plugin L1861–1949; Migrating off package rollback (1.5.16, removed in 1.5.17) L1950–2025
117
+ - Guidelines: Interface guidelines L2026–2133; Menu guidelines L2134–2230
118
+ - Reference: Manifest L2231–2298; Capabilities L2299–2357; Limits L2358–2472; Commands, menus and bindings L2473–2955; Platforms L2956–2984; Theme L2985–3206; Command line L3207–3264
119
+ - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3265–7005
120
+ - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L7006–8476
121
121
 
122
122
  ## API
123
123
 
124
124
  Where each module of `plugin` (`plugin.<module>`) is declared:
125
125
 
126
- - Plugin L3418, Scope L3287, PluginError L3226, Disposable L3199
127
- - Commands L3630; Windows L3860; Panels L4503; Dialogs L4789; Player L5256; video: VideoFilter L5563, VideoView L5626, RenderOptions L5668; audio: AudioFilter L5587; Subtitles L5766; media: MediaCapture L5927; Resolvers L6032; Packages L6274; Osd L5499; Host L5430; Clipboard L6406; Power L6426; Tools L6459; Storage L6502; System L6589; Secrets L6607; window: MainWindow L4626; App L4707; Bindings L6376; Ui L3735; Timeline L4583; Drops L4159; Appearance L4188; I18n L4330; Settings L4436.
126
+ - Plugin L3499, Scope L3368, PluginError L3307, Disposable L3280
127
+ - Commands L3744; Windows L3974; Panels L4617; Dialogs L4903; Player L5370; video: VideoFilter L5677, VideoView L5740, RenderOptions L5782; audio: AudioFilter L5701; Subtitles L5880; media: MediaCapture L6041; Resolvers L6146; Packages L6388; Osd L5613; Host L5544; Clipboard L6520; Power L6540; Tools L6573; Storage L6616; System L6703; Secrets L6721; window: MainWindow L4740; App L4821; Bindings L6490; Ui L3849; Timeline L4697; Drops L4273; Appearance L4302; I18n L4444; Settings L4550.
128
128
 
129
129
  ## Examples
130
130
 
131
131
  Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
132
132
 
133
- - hello: The README Quick start: one menu command and an on-screen message. L8721–8810
134
- - blur: A tool window with one slider that blurs the playing video. L8811–9072
135
- - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L9073–9405
136
- - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L9406–9590
137
- - panel-menu: A sidebar panel with its own right-click menu. L9591–9796
138
- - menus: Radio menu items, separator groups and a menu item whose title changes. L9797–9956
139
- - theme-widgets: A tool window drawn with the player's theme and widgets. L9957–10120
140
- - markdown: A window that shows a Markdown document. L10121–10315
141
- - i18n: A plugin in the player's language: English source text, a Chinese translation. L10316–10483
133
+ - hello: The README Quick start: one menu command and an on-screen message. L8907–8996
134
+ - blur: A tool window with one slider that blurs the playing video. L8997–9258
135
+ - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L9259–9591
136
+ - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L9592–9776
137
+ - panel-menu: A sidebar panel with its own right-click menu. L9777–9982
138
+ - menus: Radio menu items, separator groups and a menu item whose title changes. L9983–10142
139
+ - theme-widgets: A tool window drawn with the player's theme and widgets. L10143–10306
140
+ - markdown: A window that shows a Markdown document. L10307–10501
141
+ - i18n: A plugin in the player's language: English source text, a Chinese translation. L10502–10669
142
142
 
143
143
  ## Optional
144
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.28",
3
+ "version": "1.5.29",
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": {
@@ -0,0 +1,138 @@
1
+ // Command buttons of a bottom bar (player 0.5.10, SDK 1.5.29): the buttons
2
+ // that plugins' commands contribute (`commands.add({ button })`) and the
3
+ // player's sidebar toggle, rightmost. The player fills `BarButtonsHost` in
4
+ // every window it composes and runs the clicks; a bottom bar places one
5
+ // `BarButtons` in its own layout and gives it the width it can spare:
6
+ // import { BarButtons } from "@vestra/bar-buttons.slint";
7
+ // strip := BarButtons { x: parent.width - self.width - 4px; room: ...; }
8
+ // It shows as many 41×40 cells as fit in `room`, the strip as wide as the
9
+ // cells shown (`width`). Cells go from the strip's outer (left) end inward:
10
+ // of the plugins' buttons (ascending `order`, left to right) the lowest
11
+ // order goes first, and the sidebar toggle goes last. A bar without a `BarButtons` shows none of them (their menu
12
+ // entries stay; the player logs which buttons are hidden).
13
+ //
14
+ // The cells look like the bar's own icon buttons: a 32×32 plate on hover
15
+ // and press, the V-Line icon in the text colour (accent while a toggle is
16
+ // on), the bar's tooltip (ToolTipArea). They are persistent elements shown with
17
+ // `visible:`; nothing is created on hover or resize.
18
+ import { Theme } from "theme.slint";
19
+ import { Icon } from "icons.slint";
20
+ import { ToolTipArea } from "widgets.slint";
21
+
22
+ // One button as the player shows it.
23
+ export struct BarButtonItem {
24
+ // What a click runs (the player's own token; "" for no button).
25
+ token: string,
26
+ // A name of @vestra/icons.slint, or "" for `image`.
27
+ icon: string,
28
+ // The plugin's image (`plugin.ui.image`), drawn in the icon colour.
29
+ image: image,
30
+ // The command's title (the accessibility label).
31
+ label: string,
32
+ // The tooltip: the title and its key, the reason it is unavailable.
33
+ tip: string,
34
+ enabled: bool,
35
+ // The command has a checked state; `checked` draws the icon in the accent.
36
+ checkable: bool,
37
+ checked: bool,
38
+ // At half opacity: on, but not shown for want of room (the sidebar).
39
+ dimmed: bool,
40
+ }
41
+
42
+ // Filled by the player; a bar reads it only through `BarButtons`.
43
+ export global BarButtonsHost {
44
+ // The plugins' buttons, left to right, at most 8.
45
+ in property <[BarButtonItem]> buttons;
46
+ // The sidebar toggle (`token` "" while there is none).
47
+ in property <BarButtonItem> toggle;
48
+ // Set when a `BarButtons` exists in this window.
49
+ in-out property <bool> placed;
50
+ // A click on the cell with this token.
51
+ callback clicked(string);
52
+ }
53
+
54
+ // One 41×40 cell (the bar's BarButton). `shown`: on screen; a hidden cell
55
+ // is not actionable from the accessibility tree either.
56
+ component Cell inherits Rectangle {
57
+ in property <BarButtonItem> item;
58
+ in property <bool> shown;
59
+ private property <bool> live: root.shown && root.item.enabled;
60
+ private property <brush> tint: !root.item.enabled ? Theme.icon-disabled
61
+ : root.item.checked ? (touch.pressed ? Theme.accent-press : Theme.accent)
62
+ : Theme.text;
63
+ width: 41px;
64
+ height: 40px;
65
+ visible: root.shown;
66
+ opacity: root.item.dimmed ? 0.5 : 1;
67
+ accessible-role: button;
68
+ accessible-label: root.item.label;
69
+ accessible-description: root.item.tip;
70
+ accessible-enabled: root.live;
71
+ accessible-checkable: root.item.checkable;
72
+ accessible-checked: root.item.checked;
73
+ accessible-action-default => { if root.live { BarButtonsHost.clicked(root.item.token); } }
74
+ Rectangle {
75
+ x: 4px;
76
+ y: 4px;
77
+ width: parent.width - 9px;
78
+ height: parent.height - 8px;
79
+ border-radius: Theme.radius-panel;
80
+ background: !root.live ? transparent
81
+ : touch.pressed ? Theme.press-bg
82
+ : touch.has-hover ? Theme.hover-bg : transparent;
83
+ }
84
+ Icon {
85
+ visible: root.item.icon != "";
86
+ x: (root.width - 1px - self.width) / 2;
87
+ y: (root.height - self.height) / 2;
88
+ name: root.item.icon;
89
+ color: root.tint;
90
+ }
91
+ // A plugin's image in the icon box (14×12), aspect kept, coloured like the icons.
92
+ Image {
93
+ visible: root.item.icon == "";
94
+ x: (root.width - 1px - self.width) / 2;
95
+ y: (root.height - self.height) / 2;
96
+ width: 14px;
97
+ height: 12px;
98
+ source: root.item.image;
99
+ image-fit: contain;
100
+ colorize: root.tint;
101
+ }
102
+ ToolTipArea {
103
+ width: 100%;
104
+ height: 100%;
105
+ text: root.shown ? root.item.tip : "";
106
+ touch := TouchArea {
107
+ enabled: root.live;
108
+ clicked => { BarButtonsHost.clicked(root.item.token); }
109
+ }
110
+ }
111
+ }
112
+
113
+ // The strip: place it at the bar's right end and give it the width you can
114
+ // spare; it is as wide as the cells it shows (read `width` to lay out the rest).
115
+ export component BarButtons {
116
+ // The width the bar can give the strip; the cells that fit show.
117
+ in property <length> room;
118
+ // The index (in the player's list, ascending order) of the leftmost
119
+ // plugin button shown; the ones before it are hidden for want of room.
120
+ out property <int> first-shown: BarButtonsHost.buttons.length - root.count;
121
+ // The cells that fit in `room`.
122
+ private property <int> fit: Math.max(0, Math.floor(root.room / 41px));
123
+ private property <bool> toggle: BarButtonsHost.toggle.token != "" && root.fit >= 1;
124
+ // The plugins' buttons shown: the highest orders, left of the toggle's cell.
125
+ private property <int> count: Math.max(0, Math.min(BarButtonsHost.buttons.length, root.fit - (root.toggle ? 1 : 0)));
126
+ width: (root.count + (root.toggle ? 1 : 0)) * 41px;
127
+ height: 40px;
128
+ init => { BarButtonsHost.placed = true; }
129
+ Cell { x: (0 - root.first-shown) * 41px; shown: 0 >= root.first-shown && 0 < BarButtonsHost.buttons.length; item: BarButtonsHost.buttons[0]; }
130
+ Cell { x: (1 - root.first-shown) * 41px; shown: 1 >= root.first-shown && 1 < BarButtonsHost.buttons.length; item: BarButtonsHost.buttons[1]; }
131
+ Cell { x: (2 - root.first-shown) * 41px; shown: 2 >= root.first-shown && 2 < BarButtonsHost.buttons.length; item: BarButtonsHost.buttons[2]; }
132
+ Cell { x: (3 - root.first-shown) * 41px; shown: 3 >= root.first-shown && 3 < BarButtonsHost.buttons.length; item: BarButtonsHost.buttons[3]; }
133
+ Cell { x: (4 - root.first-shown) * 41px; shown: 4 >= root.first-shown && 4 < BarButtonsHost.buttons.length; item: BarButtonsHost.buttons[4]; }
134
+ Cell { x: (5 - root.first-shown) * 41px; shown: 5 >= root.first-shown && 5 < BarButtonsHost.buttons.length; item: BarButtonsHost.buttons[5]; }
135
+ Cell { x: (6 - root.first-shown) * 41px; shown: 6 >= root.first-shown && 6 < BarButtonsHost.buttons.length; item: BarButtonsHost.buttons[6]; }
136
+ Cell { x: (7 - root.first-shown) * 41px; shown: 7 >= root.first-shown && 7 < BarButtonsHost.buttons.length; item: BarButtonsHost.buttons[7]; }
137
+ Cell { x: root.count * 41px; shown: root.toggle; item: BarButtonsHost.toggle; }
138
+ }
@@ -48,6 +48,10 @@ export global Icons {
48
48
  // Four corners pointing out (enter) or in (leave), the default 14 × 12 box.
49
49
  if name == "fullscreen" { return "M 1.5 4.5 V 1.5 H 4.5 M 9.5 1.5 H 12.5 V 4.5 M 12.5 7.5 V 10.5 H 9.5 M 4.5 10.5 H 1.5 V 7.5"; }
50
50
  if name == "exit-fullscreen" { return "M 4.5 1.5 V 4.5 H 1.5 M 9.5 1.5 V 4.5 H 12.5 M 12.5 7.5 H 9.5 V 10.5 M 1.5 7.5 H 4.5 V 10.5"; }
51
+ // A shopping bag (the plugin store) and a four-point sparkle with a
52
+ // small cross (AI), the default 14 × 12 box; bar buttons use them.
53
+ if name == "store" { return "M 2.5 4 H 11.5 L 10.9 9.6 Q 10.8 10.5 9.9 10.5 H 4.1 Q 3.2 10.5 3.1 9.6 Z M 5.2 4 V 3.3 A 1.8 1.8 0 0 1 8.8 3.3 V 4"; }
54
+ if name == "ai" { return "M 6 1.5 Q 6.6 5.4 10.5 6 Q 6.6 6.6 6 10.5 Q 5.4 6.6 1.5 6 Q 5.4 5.4 6 1.5 Z M 11.5 1.2 V 3.8 M 10.2 2.5 H 12.8"; }
51
55
  return "";
52
56
  }
53
57
  // Line width.