@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 +11 -0
- package/bin/vestra-plugin.mjs +103 -0
- package/dist/commands.d.ts +47 -1
- package/dist/commands.js +77 -0
- package/dist/player-runtime.d.ts +1 -1
- package/dist/player-runtime.js +1 -1
- package/dist/plugin/commands.js +29 -2
- package/dist/plugin/testing.d.ts +52 -1
- package/dist/plugin/testing.js +67 -1
- package/dist/plugin/types.d.ts +81 -0
- package/llms-full.txt +191 -5
- package/llms.txt +22 -22
- package/package.json +1 -1
- package/slint/vestra/bar-buttons.slint +138 -0
- package/slint/vestra/icons.slint +4 -0
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.
|
package/bin/vestra-plugin.mjs
CHANGED
|
@@ -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);
|
package/dist/commands.d.ts
CHANGED
|
@@ -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
|
package/dist/player-runtime.d.ts
CHANGED
|
@@ -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";
|
package/dist/player-runtime.js
CHANGED
|
@@ -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";
|
package/dist/plugin/commands.js
CHANGED
|
@@ -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
|
-
|
|
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();
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -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
|
package/dist/plugin/testing.js
CHANGED
|
@@ -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 ?? [])])
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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–
|
|
116
|
-
- How-to: Keep a slider responsive while dragging
|
|
117
|
-
- Guidelines: Interface guidelines
|
|
118
|
-
- Reference: Manifest
|
|
119
|
-
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html):
|
|
120
|
-
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html):
|
|
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
|
|
127
|
-
- Commands
|
|
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.
|
|
134
|
-
- blur: A tool window with one slider that blurs the playing video.
|
|
135
|
-
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone.
|
|
136
|
-
- bookmark: A tool window bound to the playback state: remember the position and jump back to it.
|
|
137
|
-
- panel-menu: A sidebar panel with its own right-click menu.
|
|
138
|
-
- menus: Radio menu items, separator groups and a menu item whose title changes.
|
|
139
|
-
- theme-widgets: A tool window drawn with the player's theme and widgets.
|
|
140
|
-
- markdown: A window that shows a Markdown document.
|
|
141
|
-
- i18n: A plugin in the player's language: English source text, a Chinese translation.
|
|
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
|
@@ -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
|
+
}
|
package/slint/vestra/icons.slint
CHANGED
|
@@ -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.
|