@vestra-player/sdk 1.5.27 → 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 +20 -0
- package/bin/vestra-plugin.mjs +103 -0
- package/dist/commands.d.ts +47 -1
- package/dist/commands.js +79 -6
- 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 +54 -3
- package/dist/plugin/testing.js +69 -3
- package/dist/plugin/types.d.ts +81 -0
- package/llms-full.txt +194 -13
- 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,25 @@
|
|
|
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
|
+
|
|
14
|
+
## 1.5.28
|
|
15
|
+
|
|
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.
|
|
17
|
+
|
|
18
|
+
- **`BUILTIN_COMMANDS` and `BUILTIN_SUBMENUS` follow the player** (its agreement test failed on them):
|
|
19
|
+
- Subtitles 0.4.6: `vestra.subtitles` **Move Up** and **Move Down** have no default keys any more (they were `Alt+ArrowUp` / `Alt+ArrowDown`, which pan the video).
|
|
20
|
+
- The NES emulator is no longer a built-in plugin (it moved to the plugin repository as VestraNES, under **Games ▸ VestraNES**): its three commands (`nes.open`, `nes.stop`, `nes.window`) and the built-in **Emulator** submenu are gone. The player has no built-in Games item.
|
|
21
|
+
- Docs: *Built-in commands* regenerated; *Menu guidelines* names a domain submenu **Games** for an emulator and no longer lists Emulator at the top level. `TestHost.otherResolvers` examples use a neutral id (`example.emulator`). `llms.txt` and `llms-full.txt` regenerated.
|
|
22
|
+
|
|
3
23
|
## 1.5.27
|
|
4
24
|
|
|
5
25
|
**Who must change: nobody.** No API changes, `apiVersion` stays `vestra/1`, no player version needed. Plugins pick the fix up with the player's next SDK upgrade (the player builds the `plugin` object with its own copy); nothing in a plugin changes.
|
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).
|
|
@@ -123,7 +199,6 @@ export const BUILTIN_SUBMENUS = Object.freeze([
|
|
|
123
199
|
},
|
|
124
200
|
{ path: ["Settings", "Language"], plugins: ["vestra.preferences"] },
|
|
125
201
|
{ path: ["Copy to Clipboard"], plugins: ["vestra.playback"] },
|
|
126
|
-
{ path: ["Emulator"], plugins: ["vestra.nes"] },
|
|
127
202
|
]);
|
|
128
203
|
/** owner | id | key path, or (title) outside the menu | bindings | panel */
|
|
129
204
|
const BUILTIN_COMMAND_TABLE = `
|
|
@@ -228,8 +303,8 @@ vestra.subtitles | second-position-aboveMain | Subtitles ▸ Secondary Subtitle
|
|
|
228
303
|
vestra.subtitles | bold | Subtitles ▸ Subtitle Style ▸ Bold | Alt+B
|
|
229
304
|
vestra.subtitles | size-up | Subtitles ▸ Subtitle Style ▸ Size + | Alt+PageUp
|
|
230
305
|
vestra.subtitles | size-down | Subtitles ▸ Subtitle Style ▸ Size − | Alt+PageDown
|
|
231
|
-
vestra.subtitles | move-up | Subtitles ▸ Subtitle Style ▸ Move Up
|
|
232
|
-
vestra.subtitles | move-down | Subtitles ▸ Subtitle Style ▸ Move Down
|
|
306
|
+
vestra.subtitles | move-up | Subtitles ▸ Subtitle Style ▸ Move Up
|
|
307
|
+
vestra.subtitles | move-down | Subtitles ▸ Subtitle Style ▸ Move Down
|
|
233
308
|
vestra.subtitles | move-reset | Subtitles ▸ Subtitle Style ▸ Reset Position | Alt+Home
|
|
234
309
|
vestra.subtitles | shadow | Subtitles ▸ Subtitle Style ▸ Shadow
|
|
235
310
|
vestra.subtitles | box | Subtitles ▸ Subtitle Style ▸ Opaque Box
|
|
@@ -369,9 +444,6 @@ vestra.audio | audio.normalize | Audio ▸ Processing ▸ Normalize | Shift+N
|
|
|
369
444
|
vestra.audio | audio.clarity | Audio ▸ Processing ▸ Crystalizer | Shift+C
|
|
370
445
|
vestra.audio | audio.reset | Audio ▸ Reset
|
|
371
446
|
vestra.visualizer | audio-cover | Audio ▸ Show Album Cover for Audio Only
|
|
372
|
-
vestra.nes | nes.open | Emulator ▸ Open NES ROM…
|
|
373
|
-
vestra.nes | nes.stop | Emulator ▸ Stop NES
|
|
374
|
-
vestra.nes | nes.window | Emulator ▸ NES Tools…
|
|
375
447
|
host | fullscreen | Toggle Fullscreen | Enter, Alt+Enter, F11
|
|
376
448
|
vestra.window | pip | Screen ▸ Picture-in-Picture
|
|
377
449
|
vestra.window | mini | Screen ▸ Mini Size | \`
|
|
@@ -417,6 +489,7 @@ vestra.preferences | language.es | Settings ▸ Language ▸ Español
|
|
|
417
489
|
vestra.info | info.about | About Vestra Player… | F1
|
|
418
490
|
host | quit | Quit | Cmd+Q
|
|
419
491
|
host | exit-fullscreen | (Leave Fullscreen) | Escape
|
|
492
|
+
host | sidebar | (Toggle Sidebar)
|
|
420
493
|
vestra.info | info.brief | (Brief Info) | Shift+Tab
|
|
421
494
|
vestra.playlist | playlist.play | (Play) | Enter, P | panel
|
|
422
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
|
|
@@ -962,7 +995,7 @@ export declare class TestHost implements Transport {
|
|
|
962
995
|
* @example
|
|
963
996
|
* ```ts
|
|
964
997
|
* const host = new TestHost();
|
|
965
|
-
* host.otherResolvers.push({ owner: "
|
|
998
|
+
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["nes"] });
|
|
966
999
|
* ```
|
|
967
1000
|
*/
|
|
968
1001
|
readonly otherResolvers: {
|
|
@@ -989,7 +1022,7 @@ export declare class TestHost implements Transport {
|
|
|
989
1022
|
*
|
|
990
1023
|
* @example
|
|
991
1024
|
* ```ts
|
|
992
|
-
* host.otherResolvers.push({ owner: "
|
|
1025
|
+
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["nes"] });
|
|
993
1026
|
* host.updateFileTypes();
|
|
994
1027
|
* await host.settle();
|
|
995
1028
|
* assert.deepEqual(plugin.resolvers.fileTypes, ["nes"]);
|
|
@@ -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. */
|
|
@@ -1079,7 +1081,7 @@ export class TestHost {
|
|
|
1079
1081
|
* @example
|
|
1080
1082
|
* ```ts
|
|
1081
1083
|
* const host = new TestHost();
|
|
1082
|
-
* host.otherResolvers.push({ owner: "
|
|
1084
|
+
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["nes"] });
|
|
1083
1085
|
* ```
|
|
1084
1086
|
*/
|
|
1085
1087
|
otherResolvers = [];
|
|
@@ -1107,7 +1109,7 @@ export class TestHost {
|
|
|
1107
1109
|
*
|
|
1108
1110
|
* @example
|
|
1109
1111
|
* ```ts
|
|
1110
|
-
* host.otherResolvers.push({ owner: "
|
|
1112
|
+
* host.otherResolvers.push({ owner: "example.emulator", extensions: ["nes"] });
|
|
1111
1113
|
* host.updateFileTypes();
|
|
1112
1114
|
* await host.settle();
|
|
1113
1115
|
* assert.deepEqual(plugin.resolvers.fileTypes, ["nes"]);
|
|
@@ -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 ?? [])])
|