@vestra-player/sdk 1.5.28 → 1.5.30
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 +23 -0
- package/bin/vestra-plugin.mjs +103 -0
- package/dist/capabilities.d.ts +13 -3
- package/dist/capabilities.js +13 -3
- package/dist/commands.d.ts +94 -8
- package/dist/commands.js +124 -7
- package/dist/index.d.ts +6 -1
- package/dist/palettes.d.ts +16 -2
- package/dist/palettes.js +16 -2
- package/dist/player-runtime.d.ts +1 -1
- package/dist/player-runtime.js +1 -1
- package/dist/plugin/bindings.d.ts +27 -3
- package/dist/plugin/bindings.js +27 -3
- package/dist/plugin/commands.js +29 -2
- package/dist/plugin/osd.d.ts +13 -1
- package/dist/plugin/osd.js +13 -1
- package/dist/plugin/services.d.ts +46 -6
- package/dist/plugin/services.js +46 -6
- package/dist/plugin/testing.d.ts +403 -61
- package/dist/plugin/testing.js +408 -60
- package/dist/plugin/types.d.ts +860 -162
- package/dist/plugin/values.d.ts +27 -3
- package/dist/plugin/values.js +27 -3
- package/dist/processing.d.ts +16 -4
- package/dist/source-resolvers.d.ts +33 -8
- package/dist/subtitles.d.ts +19 -9
- package/dist/wire.d.ts +12 -3
- package/dist/wire.js +12 -3
- package/llms-full.txt +3503 -25
- package/llms.txt +26 -26
- package/package.json +1 -1
- package/slint/vestra/bar-buttons.slint +138 -0
- package/slint/vestra/icons.slint +4 -0
|
@@ -11,12 +11,36 @@ import type { Bindings } from "./types.ts";
|
|
|
11
11
|
* `"Shift+1"`). Anything else fails with code `invalid`. The player's tests
|
|
12
12
|
* check that these agree with the player.
|
|
13
13
|
*
|
|
14
|
-
* @example
|
|
14
|
+
* @example Check in a test that the plugin's default keys are well formed
|
|
15
|
+
* The test fails on a binding the player would refuse with `invalid`, such
|
|
16
|
+
* as modifiers out of order or a misspelt key name. `isBinding` applies the
|
|
17
|
+
* same grammar as the player's parser: the part after the last `+` is the
|
|
18
|
+
* input, everything before it modifiers in the order of `modifiers`; an
|
|
19
|
+
* input is a listed name or one other printable character, and `Shift`
|
|
20
|
+
* never goes with a digit or such a character.
|
|
15
21
|
* ```ts
|
|
22
|
+
* import assert from "node:assert/strict";
|
|
16
23
|
* import { BINDING_INPUTS } from "@vestra-player/sdk/testing";
|
|
17
24
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
25
|
+
* const { modifiers, letters, digits, functionKeys, named, mouse } = BINDING_INPUTS;
|
|
26
|
+
* const names = new Set([...letters, ...functionKeys, ...named, ...mouse]);
|
|
27
|
+
* function isBinding(binding: string): boolean {
|
|
28
|
+
* // "Ctrl++" is not a binding: the + key is written "Plus".
|
|
29
|
+
* const parts = binding.split("+");
|
|
30
|
+
* const input = parts.pop()!;
|
|
31
|
+
* const order = parts.map((m) => (modifiers as readonly string[]).indexOf(m));
|
|
32
|
+
* // Each modifier known, at most once, in the listed order.
|
|
33
|
+
* if (order.some((at, i) => at < 0 || (i > 0 && at <= order[i - 1]!)))
|
|
34
|
+
* return false;
|
|
35
|
+
* if (names.has(input)) return true; // letters are upper case: "A"
|
|
36
|
+
* const typed = digits.includes(input) || /^[^\p{L}\p{N}\p{C}\s+]$/u.test(input);
|
|
37
|
+
* // ">" rather than "Shift+.": the character already says Shift.
|
|
38
|
+
* return typed && !parts.includes("Shift");
|
|
39
|
+
* }
|
|
40
|
+
* for (const binding of ["Ctrl+Shift+PageUp", "Alt+Mouse:WheelUp", ">"])
|
|
41
|
+
* assert.ok(isBinding(binding), binding);
|
|
42
|
+
* for (const binding of ["Shift+Ctrl+A", "Ctrl+a", "Shift+."])
|
|
43
|
+
* assert.ok(!isBinding(binding), binding);
|
|
20
44
|
* ```
|
|
21
45
|
*
|
|
22
46
|
* @since 1.5.14
|
package/dist/plugin/bindings.js
CHANGED
|
@@ -18,12 +18,36 @@ import { failure } from "./runtime.js";
|
|
|
18
18
|
* `"Shift+1"`). Anything else fails with code `invalid`. The player's tests
|
|
19
19
|
* check that these agree with the player.
|
|
20
20
|
*
|
|
21
|
-
* @example
|
|
21
|
+
* @example Check in a test that the plugin's default keys are well formed
|
|
22
|
+
* The test fails on a binding the player would refuse with `invalid`, such
|
|
23
|
+
* as modifiers out of order or a misspelt key name. `isBinding` applies the
|
|
24
|
+
* same grammar as the player's parser: the part after the last `+` is the
|
|
25
|
+
* input, everything before it modifiers in the order of `modifiers`; an
|
|
26
|
+
* input is a listed name or one other printable character, and `Shift`
|
|
27
|
+
* never goes with a digit or such a character.
|
|
22
28
|
* ```ts
|
|
29
|
+
* import assert from "node:assert/strict";
|
|
23
30
|
* import { BINDING_INPUTS } from "@vestra-player/sdk/testing";
|
|
24
31
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
32
|
+
* const { modifiers, letters, digits, functionKeys, named, mouse } = BINDING_INPUTS;
|
|
33
|
+
* const names = new Set([...letters, ...functionKeys, ...named, ...mouse]);
|
|
34
|
+
* function isBinding(binding: string): boolean {
|
|
35
|
+
* // "Ctrl++" is not a binding: the + key is written "Plus".
|
|
36
|
+
* const parts = binding.split("+");
|
|
37
|
+
* const input = parts.pop()!;
|
|
38
|
+
* const order = parts.map((m) => (modifiers as readonly string[]).indexOf(m));
|
|
39
|
+
* // Each modifier known, at most once, in the listed order.
|
|
40
|
+
* if (order.some((at, i) => at < 0 || (i > 0 && at <= order[i - 1]!)))
|
|
41
|
+
* return false;
|
|
42
|
+
* if (names.has(input)) return true; // letters are upper case: "A"
|
|
43
|
+
* const typed = digits.includes(input) || /^[^\p{L}\p{N}\p{C}\s+]$/u.test(input);
|
|
44
|
+
* // ">" rather than "Shift+.": the character already says Shift.
|
|
45
|
+
* return typed && !parts.includes("Shift");
|
|
46
|
+
* }
|
|
47
|
+
* for (const binding of ["Ctrl+Shift+PageUp", "Alt+Mouse:WheelUp", ">"])
|
|
48
|
+
* assert.ok(isBinding(binding), binding);
|
|
49
|
+
* for (const binding of ["Shift+Ctrl+A", "Ctrl+a", "Shift+."])
|
|
50
|
+
* assert.ok(!isBinding(binding), binding);
|
|
27
51
|
* ```
|
|
28
52
|
*
|
|
29
53
|
* @since 1.5.14
|
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/osd.d.ts
CHANGED
|
@@ -8,11 +8,23 @@ import type { TemplateArgsJson, TextValueJson } from "../wire.ts";
|
|
|
8
8
|
* rejects, `OsdPanel.set` reports to `Scope.onError`. The player's tests
|
|
9
9
|
* check that these agree with the player.
|
|
10
10
|
*
|
|
11
|
-
* @example
|
|
11
|
+
* @example Keep a long list inside the information block's limits
|
|
12
|
+
* The block shows the first 32 rows of a list, each cut at 60 columns. The
|
|
13
|
+
* player counts the text as shown (after it is translated and formatted); a
|
|
14
|
+
* block over `panelLines` lines or `panelBytes` bytes is refused whole with
|
|
15
|
+
* `invalid`, reported to `Scope.onError` because `set` never throws, so
|
|
16
|
+
* the plugin trims the list itself (for very long non-ASCII rows, check the
|
|
17
|
+
* bytes too).
|
|
12
18
|
* ```ts
|
|
19
|
+
* import type { Plugin } from "@vestra-player/sdk";
|
|
20
|
+
* declare const plugin: Plugin;
|
|
21
|
+
* declare const rows: string[];
|
|
22
|
+
* // ---cut---
|
|
13
23
|
* import { OSD_LIMITS } from "@vestra-player/sdk/testing";
|
|
14
24
|
*
|
|
25
|
+
* const panel = plugin.osd.panel();
|
|
15
26
|
* const lines = rows.slice(0, OSD_LIMITS.panelLines);
|
|
27
|
+
* // maxColumns cuts each longer line and ends it with "…".
|
|
16
28
|
* panel.set(lines.join("\n"), null, { maxColumns: 60 });
|
|
17
29
|
* ```
|
|
18
30
|
*
|
package/dist/plugin/osd.js
CHANGED
|
@@ -9,11 +9,23 @@ import { isTextValue, textProblem } from "../i18n.js";
|
|
|
9
9
|
* rejects, `OsdPanel.set` reports to `Scope.onError`. The player's tests
|
|
10
10
|
* check that these agree with the player.
|
|
11
11
|
*
|
|
12
|
-
* @example
|
|
12
|
+
* @example Keep a long list inside the information block's limits
|
|
13
|
+
* The block shows the first 32 rows of a list, each cut at 60 columns. The
|
|
14
|
+
* player counts the text as shown (after it is translated and formatted); a
|
|
15
|
+
* block over `panelLines` lines or `panelBytes` bytes is refused whole with
|
|
16
|
+
* `invalid`, reported to `Scope.onError` because `set` never throws, so
|
|
17
|
+
* the plugin trims the list itself (for very long non-ASCII rows, check the
|
|
18
|
+
* bytes too).
|
|
13
19
|
* ```ts
|
|
20
|
+
* import type { Plugin } from "@vestra-player/sdk";
|
|
21
|
+
* declare const plugin: Plugin;
|
|
22
|
+
* declare const rows: string[];
|
|
23
|
+
* // ---cut---
|
|
14
24
|
* import { OSD_LIMITS } from "@vestra-player/sdk/testing";
|
|
15
25
|
*
|
|
26
|
+
* const panel = plugin.osd.panel();
|
|
16
27
|
* const lines = rows.slice(0, OSD_LIMITS.panelLines);
|
|
28
|
+
* // maxColumns cuts each longer line and ends it with "…".
|
|
17
29
|
* panel.set(lines.join("\n"), null, { maxColumns: 60 });
|
|
18
30
|
* ```
|
|
19
31
|
*
|
|
@@ -13,13 +13,31 @@ export declare function tools(scope: ScopeImpl): Tools;
|
|
|
13
13
|
* with the key and the request around the value). The player's tests check
|
|
14
14
|
* that these agree with the player.
|
|
15
15
|
*
|
|
16
|
-
* @example
|
|
16
|
+
* @example Remember a position per URL without breaking the limits
|
|
17
|
+
* The plugin keeps the last position of each URL for this player session
|
|
18
|
+
* and skips a URL too long to be a key. Keys are counted in UTF-8 bytes, so
|
|
19
|
+
* the length is measured with `Buffer.byteLength`, not `length`; a `set`
|
|
20
|
+
* that would take the plugin's entries over 1 MiB rejects with `limit` and
|
|
21
|
+
* changes nothing, so the plugin clears the old entries and goes on.
|
|
17
22
|
* ```ts
|
|
18
|
-
* import
|
|
23
|
+
* import type { Plugin, PluginError } from "@vestra-player/sdk";
|
|
24
|
+
* declare const plugin: Plugin;
|
|
25
|
+
* declare const url: string;
|
|
26
|
+
* declare const position: number;
|
|
27
|
+
* // ---cut---
|
|
19
28
|
* import { SESSION_STORAGE_LIMITS } from "@vestra-player/sdk/testing";
|
|
20
29
|
*
|
|
21
|
-
* const
|
|
22
|
-
*
|
|
30
|
+
* const session = plugin.storage.session;
|
|
31
|
+
* if (Buffer.byteLength(url) <= SESSION_STORAGE_LIMITS.keyBytes) {
|
|
32
|
+
* try {
|
|
33
|
+
* await session.set(url, position);
|
|
34
|
+
* } catch (error) {
|
|
35
|
+
* if ((error as PluginError).code !== "limit") throw error;
|
|
36
|
+
* // Full: start over rather than lose the newest position.
|
|
37
|
+
* for (const key of await session.keys()) await session.delete(key);
|
|
38
|
+
* await session.set(url, position);
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
23
41
|
* ```
|
|
24
42
|
*
|
|
25
43
|
* @since 1.5.25
|
|
@@ -46,11 +64,33 @@ export declare function system(scope: ScopeImpl): System;
|
|
|
46
64
|
* fails with code `invalid`; a new name beyond `names` fails with `limit`.
|
|
47
65
|
* The player's tests check that these agree with the player.
|
|
48
66
|
*
|
|
49
|
-
* @example
|
|
67
|
+
* @example Store an API token only when it fits
|
|
68
|
+
* The plugin keeps a token the user pasted, or tells them it is empty or
|
|
69
|
+
* too long. Values are 1–1024 bytes counted in UTF-8, so the check uses
|
|
70
|
+
* `Buffer.byteLength`; `secrets.set` would refuse other values with
|
|
71
|
+
* `invalid`. The store itself can still refuse (`unsupported` without a
|
|
72
|
+
* system store, `unavailable` or `failed` when it denies access): only
|
|
73
|
+
* those codes get the "could not be saved" message, anything else (a
|
|
74
|
+
* missing `secrets` capability) is a bug and is thrown on.
|
|
50
75
|
* ```ts
|
|
76
|
+
* import type { Plugin, PluginError } from "@vestra-player/sdk";
|
|
77
|
+
* declare const plugin: Plugin;
|
|
78
|
+
* declare const token: string;
|
|
79
|
+
* // ---cut---
|
|
51
80
|
* import { SECRET_LIMITS } from "@vestra-player/sdk/testing";
|
|
52
81
|
*
|
|
53
|
-
*
|
|
82
|
+
* if (!token || Buffer.byteLength(token) > SECRET_LIMITS.valueBytes) {
|
|
83
|
+
* await plugin.osd.show("Paste a token of at most 1024 bytes.");
|
|
84
|
+
* } else {
|
|
85
|
+
* try {
|
|
86
|
+
* await plugin.secrets.set("api-token", token);
|
|
87
|
+
* } catch (error) {
|
|
88
|
+
* const { code } = error as PluginError;
|
|
89
|
+
* if (!["unsupported", "unavailable", "failed"].includes(code)) throw error;
|
|
90
|
+
* // No keychain or access denied: ask again next time.
|
|
91
|
+
* await plugin.osd.show("The token could not be saved.");
|
|
92
|
+
* }
|
|
93
|
+
* }
|
|
54
94
|
* ```
|
|
55
95
|
*
|
|
56
96
|
* @since 1.5.14
|
package/dist/plugin/services.js
CHANGED
|
@@ -247,13 +247,31 @@ class Store {
|
|
|
247
247
|
* with the key and the request around the value). The player's tests check
|
|
248
248
|
* that these agree with the player.
|
|
249
249
|
*
|
|
250
|
-
* @example
|
|
250
|
+
* @example Remember a position per URL without breaking the limits
|
|
251
|
+
* The plugin keeps the last position of each URL for this player session
|
|
252
|
+
* and skips a URL too long to be a key. Keys are counted in UTF-8 bytes, so
|
|
253
|
+
* the length is measured with `Buffer.byteLength`, not `length`; a `set`
|
|
254
|
+
* that would take the plugin's entries over 1 MiB rejects with `limit` and
|
|
255
|
+
* changes nothing, so the plugin clears the old entries and goes on.
|
|
251
256
|
* ```ts
|
|
252
|
-
* import
|
|
257
|
+
* import type { Plugin, PluginError } from "@vestra-player/sdk";
|
|
258
|
+
* declare const plugin: Plugin;
|
|
259
|
+
* declare const url: string;
|
|
260
|
+
* declare const position: number;
|
|
261
|
+
* // ---cut---
|
|
253
262
|
* import { SESSION_STORAGE_LIMITS } from "@vestra-player/sdk/testing";
|
|
254
263
|
*
|
|
255
|
-
* const
|
|
256
|
-
*
|
|
264
|
+
* const session = plugin.storage.session;
|
|
265
|
+
* if (Buffer.byteLength(url) <= SESSION_STORAGE_LIMITS.keyBytes) {
|
|
266
|
+
* try {
|
|
267
|
+
* await session.set(url, position);
|
|
268
|
+
* } catch (error) {
|
|
269
|
+
* if ((error as PluginError).code !== "limit") throw error;
|
|
270
|
+
* // Full: start over rather than lose the newest position.
|
|
271
|
+
* for (const key of await session.keys()) await session.delete(key);
|
|
272
|
+
* await session.set(url, position);
|
|
273
|
+
* }
|
|
274
|
+
* }
|
|
257
275
|
* ```
|
|
258
276
|
*
|
|
259
277
|
* @since 1.5.25
|
|
@@ -390,11 +408,33 @@ export function system(scope) {
|
|
|
390
408
|
* fails with code `invalid`; a new name beyond `names` fails with `limit`.
|
|
391
409
|
* The player's tests check that these agree with the player.
|
|
392
410
|
*
|
|
393
|
-
* @example
|
|
411
|
+
* @example Store an API token only when it fits
|
|
412
|
+
* The plugin keeps a token the user pasted, or tells them it is empty or
|
|
413
|
+
* too long. Values are 1–1024 bytes counted in UTF-8, so the check uses
|
|
414
|
+
* `Buffer.byteLength`; `secrets.set` would refuse other values with
|
|
415
|
+
* `invalid`. The store itself can still refuse (`unsupported` without a
|
|
416
|
+
* system store, `unavailable` or `failed` when it denies access): only
|
|
417
|
+
* those codes get the "could not be saved" message, anything else (a
|
|
418
|
+
* missing `secrets` capability) is a bug and is thrown on.
|
|
394
419
|
* ```ts
|
|
420
|
+
* import type { Plugin, PluginError } from "@vestra-player/sdk";
|
|
421
|
+
* declare const plugin: Plugin;
|
|
422
|
+
* declare const token: string;
|
|
423
|
+
* // ---cut---
|
|
395
424
|
* import { SECRET_LIMITS } from "@vestra-player/sdk/testing";
|
|
396
425
|
*
|
|
397
|
-
*
|
|
426
|
+
* if (!token || Buffer.byteLength(token) > SECRET_LIMITS.valueBytes) {
|
|
427
|
+
* await plugin.osd.show("Paste a token of at most 1024 bytes.");
|
|
428
|
+
* } else {
|
|
429
|
+
* try {
|
|
430
|
+
* await plugin.secrets.set("api-token", token);
|
|
431
|
+
* } catch (error) {
|
|
432
|
+
* const { code } = error as PluginError;
|
|
433
|
+
* if (!["unsupported", "unavailable", "failed"].includes(code)) throw error;
|
|
434
|
+
* // No keychain or access denied: ask again next time.
|
|
435
|
+
* await plugin.osd.show("The token could not be saved.");
|
|
436
|
+
* }
|
|
437
|
+
* }
|
|
398
438
|
* ```
|
|
399
439
|
*
|
|
400
440
|
* @since 1.5.14
|