@vestra-player/sdk 1.5.9 → 1.5.12
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 +35 -0
- package/README.md +1 -1
- package/bin/vestra-plugin.mjs +4 -2
- package/dist/commands.d.ts +7 -4
- package/dist/i18n-tools.js +120 -12
- package/dist/i18n.d.ts +92 -0
- package/dist/i18n.js +299 -0
- package/dist/player-runtime.d.ts +2 -1
- package/dist/player-runtime.js +2 -1
- package/dist/plugin/bindings.js +1 -0
- package/dist/plugin/commands.js +24 -4
- package/dist/plugin/index.js +9 -1
- package/dist/plugin/language.d.ts +6 -2
- package/dist/plugin/language.js +74 -0
- package/dist/plugin/osd.d.ts +10 -2
- package/dist/plugin/osd.js +35 -16
- package/dist/plugin/subtitles.js +9 -1
- package/dist/plugin/testing.d.ts +69 -11
- package/dist/plugin/testing.js +234 -58
- package/dist/plugin/timeline.js +3 -1
- package/dist/plugin/types.d.ts +338 -43
- package/dist/plugin/ui.js +26 -10
- package/dist/plugin/values.d.ts +55 -0
- package/dist/plugin/values.js +117 -0
- package/dist/source-resolvers.d.ts +0 -1
- package/dist/source-resolvers.js +9 -1
- package/dist/subtitles.d.ts +3 -1
- package/dist/wire.d.ts +17 -9
- package/dist/wire.js +2 -1
- package/llms-full.txt +298 -49
- package/llms.txt +42 -42
- package/package.json +1 -1
- package/slint/vestra/widgets.slint +6 -2
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -3,17 +3,36 @@ 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
5
|
import { COMMAND_LIMITS, MENU_IDS } from "../commands.ts";
|
|
6
|
+
import { UI_VALUE_LIMITS } from "./values.ts";
|
|
6
7
|
import type { CommandDeclaration } from "../commands.ts";
|
|
7
8
|
import type { SourceState } from "../source.ts";
|
|
8
9
|
import type { SubtitleState } from "../subtitles.ts";
|
|
9
10
|
import type { RenderSettings, ViewSettings } from "./types.ts";
|
|
10
11
|
import type { MediaJob } from "../media.ts";
|
|
11
|
-
import type { LanguagePreference, Locale, Plugin, TemplateArgs, ThemeId } from "./types.ts";
|
|
12
|
-
import { LOCALES } from "../i18n.ts";
|
|
12
|
+
import type { LanguagePreference, Locale, Plugin, TemplateArgs, TextValue, ThemeId } from "./types.ts";
|
|
13
|
+
import { LOCALES, LIST_SEPARATORS, TEXT_LIMITS } from "../i18n.ts";
|
|
13
14
|
/** Throw one of these from `host.handle(...)` to simulate a definite refusal. */
|
|
14
15
|
export { HostError };
|
|
15
16
|
/** The player's command caps, which the test host enforces as well. */
|
|
16
17
|
export { COMMAND_LIMITS, MENU_IDS };
|
|
18
|
+
/** The player's property value limits, which the test host enforces as well. */
|
|
19
|
+
export { UI_VALUE_LIMITS };
|
|
20
|
+
/**
|
|
21
|
+
* The player's limits on one text value ({@link Text}, {@link TextList},
|
|
22
|
+
* with everything nested in it), which the test host enforces as well:
|
|
23
|
+
* nesting depth 8, 1024 texts, lists and arguments in all, 16 arguments per
|
|
24
|
+
* text, 256 items per list, 4096 bytes per source text or string argument.
|
|
25
|
+
*
|
|
26
|
+
* @since 1.5.11
|
|
27
|
+
*/
|
|
28
|
+
export { TEXT_LIMITS };
|
|
29
|
+
/**
|
|
30
|
+
* The separator of each {@link ListStyle} per interface language, as the
|
|
31
|
+
* player joins a {@link TextList}.
|
|
32
|
+
*
|
|
33
|
+
* @since 1.5.11
|
|
34
|
+
*/
|
|
35
|
+
export { LIST_SEPARATORS };
|
|
17
36
|
/**
|
|
18
37
|
* The player's interface languages, English (the source) first, as the
|
|
19
38
|
* player's own table: `["en", "zh-Hans", "ja", "ko", "de", "fr", "es"]`.
|
|
@@ -76,6 +95,19 @@ export interface FakeWindow {
|
|
|
76
95
|
*/
|
|
77
96
|
userClose(): void;
|
|
78
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* A file dialog as the player shows it ({@link TestHost.fileDialogs}).
|
|
100
|
+
*
|
|
101
|
+
* @since 1.5.11
|
|
102
|
+
*/
|
|
103
|
+
export interface FileDialogText {
|
|
104
|
+
/** Which dialog. */
|
|
105
|
+
kind: "openFile" | "openFiles" | "openDirectory" | "saveFile";
|
|
106
|
+
/** Its title as shown, if the plugin gave one. */
|
|
107
|
+
title?: string;
|
|
108
|
+
/** The proposed file name as shown, if the plugin gave one. */
|
|
109
|
+
fileName?: string;
|
|
110
|
+
}
|
|
79
111
|
/**
|
|
80
112
|
* A command the plugin registered with `plugin.commands.add`, as the test
|
|
81
113
|
* host keeps it. Get one with {@link TestHost.command} (the plugin's own) or
|
|
@@ -110,6 +142,14 @@ export interface FakeCommand {
|
|
|
110
142
|
title: string;
|
|
111
143
|
/** Each segment of the path as shown; empty without a path. */
|
|
112
144
|
path: string[];
|
|
145
|
+
/**
|
|
146
|
+
* The disabled or busy reason as shown, `null` without one (a plugin
|
|
147
|
+
* command's; not enumerable, so comparing `display` with
|
|
148
|
+
* `{ title, path }` still works).
|
|
149
|
+
*
|
|
150
|
+
* @since 1.5.11
|
|
151
|
+
*/
|
|
152
|
+
reason?: string | null;
|
|
113
153
|
};
|
|
114
154
|
/** The command state the player last received (after the update pass). */
|
|
115
155
|
state: {
|
|
@@ -119,8 +159,8 @@ export interface FakeCommand {
|
|
|
119
159
|
checked: boolean | null;
|
|
120
160
|
/** `true` while the plugin reports the command as busy. */
|
|
121
161
|
busy: boolean;
|
|
122
|
-
/** The reason
|
|
123
|
-
reason:
|
|
162
|
+
/** The reason the plugin sent (source text or a text value; see `display.reason`). */
|
|
163
|
+
reason: TextValue | null;
|
|
124
164
|
};
|
|
125
165
|
/**
|
|
126
166
|
* The state revision the player holds; increases with every state update
|
|
@@ -348,7 +388,7 @@ export declare class TestHost implements Transport {
|
|
|
348
388
|
*
|
|
349
389
|
* @since 1.5.9
|
|
350
390
|
*/
|
|
351
|
-
translate(text:
|
|
391
|
+
translate(text: TextValue, args?: TemplateArgs): string;
|
|
352
392
|
/**
|
|
353
393
|
* The saved interface language (`plugin.appearance.language`), changed by
|
|
354
394
|
* `appearance.setLanguage` or {@link TestHost.setLanguage}.
|
|
@@ -383,6 +423,15 @@ export declare class TestHost implements Transport {
|
|
|
383
423
|
/** The checkbox, if any. */
|
|
384
424
|
checkbox?: string;
|
|
385
425
|
}[];
|
|
426
|
+
/**
|
|
427
|
+
* Every file dialog (`openFile`, `openFiles`, `openDirectory`,
|
|
428
|
+
* `saveFile`) as the player shows it: the title translated, a text file
|
|
429
|
+
* name translated, newest last. The options the plugin sent are in
|
|
430
|
+
* `host.sent("dialogs.open")`.
|
|
431
|
+
*
|
|
432
|
+
* @since 1.5.11
|
|
433
|
+
*/
|
|
434
|
+
readonly fileDialogs: FileDialogText[];
|
|
386
435
|
/** The sequence provider's flags, `null` without a provider. */
|
|
387
436
|
sequence: {
|
|
388
437
|
/** The provider's `canNext`, as last evaluated. */
|
|
@@ -1193,11 +1242,20 @@ export declare class TestHost implements Transport {
|
|
|
1193
1242
|
failStream(producerId: number, message: string): void;
|
|
1194
1243
|
/**
|
|
1195
1244
|
* Press a binding like the host's table would: panel-focus bindings first
|
|
1196
|
-
* when `panel` is true, then global ones;
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
* (
|
|
1200
|
-
*
|
|
1245
|
+
* when `panel` is true, then global ones; within a scope the earliest
|
|
1246
|
+
* registration first (the player's own commands before the plugin's), as
|
|
1247
|
+
* the conflict rule orders them. A disabled command lets the key pass to
|
|
1248
|
+
* the next command bound to it (SDK 1.5.12, as player 0.4.3): the one it
|
|
1249
|
+
* shadowed, then the global scope. The first enabled one runs; `press`
|
|
1250
|
+
* resolves after it ran (`"ran"`), or rejects when it is busy. When only
|
|
1251
|
+
* disabled commands hold the key, the first one with a `reason` shows the
|
|
1252
|
+
* reason on the OSD and eats the key (`"reason"`, see `host.osd`);
|
|
1253
|
+
* without a reason the key goes on to the focused control (`"passed"`,
|
|
1254
|
+
* nothing runs). Rejects when nothing is bound. Bindings use the
|
|
1255
|
+
* reference syntax (`"Space"`, `"Ctrl+O"`, `"Mouse:WheelUp"`), and user
|
|
1256
|
+
* overrides in `bindings.overlay` apply.
|
|
1257
|
+
*
|
|
1258
|
+
* @returns What the key did: `"ran"`, `"reason"` or `"passed"`.
|
|
1201
1259
|
*
|
|
1202
1260
|
* @example
|
|
1203
1261
|
* ```ts
|
|
@@ -1207,7 +1265,7 @@ export declare class TestHost implements Transport {
|
|
|
1207
1265
|
*/
|
|
1208
1266
|
press(binding: string, options?: {
|
|
1209
1267
|
panel?: boolean;
|
|
1210
|
-
}): Promise<
|
|
1268
|
+
}): Promise<"ran" | "reason" | "passed">;
|
|
1211
1269
|
/**
|
|
1212
1270
|
* Bindings declared by more than one command, each as
|
|
1213
1271
|
* `[binding, winner, ...ignored]` (command ids; core actions as
|
package/dist/plugin/testing.js
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* value ranges (an out-of-range `filter.set` ends in `failed` with
|
|
19
19
|
* `invalid`), an empty chain removes the plugin's chain, a chain on a
|
|
20
20
|
* generated stream fails `unsupported` (the player's own conformance
|
|
21
|
-
* cases check all three), and the command limits ({@link COMMAND_LIMITS})
|
|
22
|
-
*
|
|
21
|
+
* cases check all three), and the command limits ({@link COMMAND_LIMITS}),
|
|
22
|
+
* property value limits ({@link UI_VALUE_LIMITS}) and menu ids
|
|
23
|
+
* ({@link MENU_IDS}) are enforced.
|
|
23
24
|
*
|
|
24
25
|
* Typical workflow: start the plugin with {@link startPlugin} (it reads the
|
|
25
26
|
* plugin's `manifest.json`), drive it the way the user or another plugin
|
|
@@ -65,15 +66,34 @@ import { extname, isAbsolute, join } from "node:path";
|
|
|
65
66
|
import { fileURLToPath } from "node:url";
|
|
66
67
|
import { HostError } from "../wire.js";
|
|
67
68
|
import { COMMAND_LIMITS, MENU_IDS } from "../commands.js";
|
|
69
|
+
import { UI_VALUE_LIMITS, valueLimitProblem } from "./values.js";
|
|
68
70
|
import { nodeProblem } from "../processing.js";
|
|
69
71
|
import { RENDER_DEFAULTS, renderProblem, VIEW_DEFAULTS, viewProblem, } from "./view.js";
|
|
70
72
|
import { createPlugin } from "./index.js";
|
|
71
73
|
import { THEMES } from "./appearance.js";
|
|
72
|
-
import { Catalog,
|
|
74
|
+
import { Catalog, format as formatTemplate, HOST_GROUPS, HOST_ZH_HANS, LOCALES, fitColumns, isTextValue, LIST_SEPARATORS, loadPackageCatalog, menuKeys, renderTemplateArgs, renderText, resolveTexts, TEXT_LIMITS, textKey, textProblem, } from "../i18n.js";
|
|
73
75
|
/** Throw one of these from `host.handle(...)` to simulate a definite refusal. */
|
|
74
76
|
export { HostError };
|
|
75
77
|
/** The player's command caps, which the test host enforces as well. */
|
|
76
78
|
export { COMMAND_LIMITS, MENU_IDS };
|
|
79
|
+
/** The player's property value limits, which the test host enforces as well. */
|
|
80
|
+
export { UI_VALUE_LIMITS };
|
|
81
|
+
/**
|
|
82
|
+
* The player's limits on one text value ({@link Text}, {@link TextList},
|
|
83
|
+
* with everything nested in it), which the test host enforces as well:
|
|
84
|
+
* nesting depth 8, 1024 texts, lists and arguments in all, 16 arguments per
|
|
85
|
+
* text, 256 items per list, 4096 bytes per source text or string argument.
|
|
86
|
+
*
|
|
87
|
+
* @since 1.5.11
|
|
88
|
+
*/
|
|
89
|
+
export { TEXT_LIMITS };
|
|
90
|
+
/**
|
|
91
|
+
* The separator of each {@link ListStyle} per interface language, as the
|
|
92
|
+
* player joins a {@link TextList}.
|
|
93
|
+
*
|
|
94
|
+
* @since 1.5.11
|
|
95
|
+
*/
|
|
96
|
+
export { LIST_SEPARATORS };
|
|
77
97
|
/**
|
|
78
98
|
* The player's interface languages, English (the source) first, as the
|
|
79
99
|
* player's own table: `["en", "zh-Hans", "ja", "ko", "de", "fr", "es"]`.
|
|
@@ -303,16 +323,7 @@ export class TestHost {
|
|
|
303
323
|
* @since 1.5.9
|
|
304
324
|
*/
|
|
305
325
|
loadCatalog(directory) {
|
|
306
|
-
this.#catalog =
|
|
307
|
-
if (this.locale === "en")
|
|
308
|
-
return;
|
|
309
|
-
const base = join(directory, "locales", this.locale);
|
|
310
|
-
if (existsSync(`${base}.json`)) {
|
|
311
|
-
const compiled = JSON.parse(readFileSync(`${base}.json`, "utf8"));
|
|
312
|
-
this.#catalog = new Catalog(compiled.messages ?? {});
|
|
313
|
-
}
|
|
314
|
-
else if (existsSync(`${base}.po`))
|
|
315
|
-
this.#catalog = new Catalog(compileCatalog(this.locale, parsePo(readFileSync(`${base}.po`, "utf8")).entries).messages);
|
|
326
|
+
this.#catalog = loadPackageCatalog(directory, this.locale);
|
|
316
327
|
}
|
|
317
328
|
/**
|
|
318
329
|
* A text of the plugin as the player shows it: translated with the
|
|
@@ -331,25 +342,69 @@ export class TestHost {
|
|
|
331
342
|
* @since 1.5.9
|
|
332
343
|
*/
|
|
333
344
|
translate(text, args) {
|
|
334
|
-
|
|
345
|
+
if (typeof text !== "string")
|
|
346
|
+
return renderText(text, this.#catalog, this.locale);
|
|
347
|
+
const values = renderTemplateArgs(args, this.#catalog, this.locale);
|
|
348
|
+
return this.#catalog.template(text, values ?? null);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* A text of a request as the player shows it, refused as the player
|
|
352
|
+
* refuses it: a string is a template (formatted with `args`, if given), a
|
|
353
|
+
* text value carries its own arguments.
|
|
354
|
+
*/
|
|
355
|
+
#shown(text, args, what) {
|
|
356
|
+
if (args !== undefined && args !== null) {
|
|
357
|
+
const problem = textProblem(args, "args");
|
|
358
|
+
if (problem)
|
|
359
|
+
throw new HostError("invalid", problem);
|
|
360
|
+
}
|
|
361
|
+
if (typeof text === "string")
|
|
362
|
+
return this.translate(text, (args ?? undefined));
|
|
363
|
+
if (!isTextValue(text))
|
|
364
|
+
throw new HostError("invalid", `${what} requires a string or a text`);
|
|
365
|
+
const problem = textProblem(text);
|
|
366
|
+
if (problem)
|
|
367
|
+
throw new HostError("invalid", `${what}: ${problem}`);
|
|
368
|
+
if (args !== undefined && args !== null)
|
|
369
|
+
throw new HostError("invalid", `${what}: a text value takes no args`);
|
|
370
|
+
return this.translate(text);
|
|
371
|
+
}
|
|
372
|
+
/** A declared text: a string is translated as it is, a text rendered. */
|
|
373
|
+
#declared(text, what) {
|
|
374
|
+
return this.#shown(text, undefined, what);
|
|
375
|
+
}
|
|
376
|
+
/** Property values with their text values shown, as the player does first. */
|
|
377
|
+
#shownValues(values) {
|
|
378
|
+
const resolved = resolveTexts(values, this.#catalog, this.locale);
|
|
379
|
+
if ("problem" in resolved)
|
|
380
|
+
throw new HostError("invalid", resolved.problem);
|
|
381
|
+
return resolved.value;
|
|
335
382
|
}
|
|
336
383
|
/** A text of the player itself, in the player's language (see `locale`). */
|
|
337
384
|
#hostText(text, args) {
|
|
338
385
|
const zh = this.#locale === undefined || this.#locale === "zh-Hans";
|
|
339
386
|
const shown = (zh && (HOST_ZH_HANS[text] ?? TEST_ZH_HANS[text])) || text;
|
|
340
|
-
return args === undefined
|
|
387
|
+
return args === undefined
|
|
388
|
+
? shown
|
|
389
|
+
: formatTemplate(shown, args);
|
|
341
390
|
}
|
|
342
391
|
/** A declared menu path and title as the player shows them. */
|
|
343
|
-
#display(path, title) {
|
|
344
|
-
const keys = menuKeys(path);
|
|
345
|
-
|
|
392
|
+
#display(path, title, reason = null) {
|
|
393
|
+
const keys = menuKeys(path.map((segment) => typeof segment === "string" ? segment : textKey(segment)));
|
|
394
|
+
const display = {
|
|
346
395
|
title: this.translate(title),
|
|
347
396
|
// The player's submenus by key, everything else as declared.
|
|
348
397
|
path: keys.map((segment, index) => index < keys.length - 1 &&
|
|
349
398
|
HOST_GROUPS.includes(keys.slice(0, index + 1).join("/"))
|
|
350
399
|
? this.#hostText(segment)
|
|
351
|
-
: this.translate(path[index] ?? segment)),
|
|
400
|
+
: this.translate((path[index] ?? segment))),
|
|
352
401
|
};
|
|
402
|
+
// Not enumerable: `deepEqual(display, { title, path })` keeps passing.
|
|
403
|
+
Object.defineProperty(display, "reason", {
|
|
404
|
+
value: reason === null ? null : this.translate(reason),
|
|
405
|
+
enumerable: false,
|
|
406
|
+
});
|
|
407
|
+
return display;
|
|
353
408
|
}
|
|
354
409
|
/**
|
|
355
410
|
* The saved interface language (`plugin.appearance.language`), changed by
|
|
@@ -377,6 +432,15 @@ export class TestHost {
|
|
|
377
432
|
* @since 1.5.9
|
|
378
433
|
*/
|
|
379
434
|
confirmTexts = [];
|
|
435
|
+
/**
|
|
436
|
+
* Every file dialog (`openFile`, `openFiles`, `openDirectory`,
|
|
437
|
+
* `saveFile`) as the player shows it: the title translated, a text file
|
|
438
|
+
* name translated, newest last. The options the plugin sent are in
|
|
439
|
+
* `host.sent("dialogs.open")`.
|
|
440
|
+
*
|
|
441
|
+
* @since 1.5.11
|
|
442
|
+
*/
|
|
443
|
+
fileDialogs = [];
|
|
380
444
|
/** The sequence provider's flags, `null` without a provider. */
|
|
381
445
|
sequence = null;
|
|
382
446
|
/**
|
|
@@ -578,6 +642,42 @@ export class TestHost {
|
|
|
578
642
|
trustedExecution: true,
|
|
579
643
|
};
|
|
580
644
|
}
|
|
645
|
+
/** A command state's reason, refused as the player refuses it. */
|
|
646
|
+
#checkReason(state) {
|
|
647
|
+
const reason = state?.reason;
|
|
648
|
+
if (reason !== null && reason !== undefined)
|
|
649
|
+
this.#declared(reason, "reason");
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Refuse property values over the player's limits ({@link UI_VALUE_LIMITS})
|
|
653
|
+
* with `limit`, naming the property, as the player does.
|
|
654
|
+
*/
|
|
655
|
+
#checkValues(values) {
|
|
656
|
+
for (const [property, value] of Object.entries(values)) {
|
|
657
|
+
const problem = valueLimitProblem(value);
|
|
658
|
+
if (problem)
|
|
659
|
+
throw new HostError("limit", `${property}: ${problem}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* A panel's or settings page's initial properties, as the player keeps
|
|
664
|
+
* them: one over the limits is left out (the property keeps its default)
|
|
665
|
+
* and reported with `ui.error`, the request itself succeeds.
|
|
666
|
+
*/
|
|
667
|
+
#initialValues(values) {
|
|
668
|
+
const kept = {};
|
|
669
|
+
for (const [property, value] of Object.entries(values)) {
|
|
670
|
+
const problem = valueLimitProblem(value);
|
|
671
|
+
if (problem)
|
|
672
|
+
this.emit("ui.error", {
|
|
673
|
+
code: "limit",
|
|
674
|
+
message: `${property}: ${problem}`,
|
|
675
|
+
});
|
|
676
|
+
else
|
|
677
|
+
kept[property] = value;
|
|
678
|
+
}
|
|
679
|
+
return kept;
|
|
680
|
+
}
|
|
581
681
|
#seeImages(values) {
|
|
582
682
|
for (const value of Object.values(values)) {
|
|
583
683
|
const path = this.imagePath(value);
|
|
@@ -1649,11 +1749,20 @@ export class TestHost {
|
|
|
1649
1749
|
}
|
|
1650
1750
|
/**
|
|
1651
1751
|
* Press a binding like the host's table would: panel-focus bindings first
|
|
1652
|
-
* when `panel` is true, then global ones;
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1655
|
-
* (
|
|
1656
|
-
*
|
|
1752
|
+
* when `panel` is true, then global ones; within a scope the earliest
|
|
1753
|
+
* registration first (the player's own commands before the plugin's), as
|
|
1754
|
+
* the conflict rule orders them. A disabled command lets the key pass to
|
|
1755
|
+
* the next command bound to it (SDK 1.5.12, as player 0.4.3): the one it
|
|
1756
|
+
* shadowed, then the global scope. The first enabled one runs; `press`
|
|
1757
|
+
* resolves after it ran (`"ran"`), or rejects when it is busy. When only
|
|
1758
|
+
* disabled commands hold the key, the first one with a `reason` shows the
|
|
1759
|
+
* reason on the OSD and eats the key (`"reason"`, see `host.osd`);
|
|
1760
|
+
* without a reason the key goes on to the focused control (`"passed"`,
|
|
1761
|
+
* nothing runs). Rejects when nothing is bound. Bindings use the
|
|
1762
|
+
* reference syntax (`"Space"`, `"Ctrl+O"`, `"Mouse:WheelUp"`), and user
|
|
1763
|
+
* overrides in `bindings.overlay` apply.
|
|
1764
|
+
*
|
|
1765
|
+
* @returns What the key did: `"ran"`, `"reason"` or `"passed"`.
|
|
1657
1766
|
*
|
|
1658
1767
|
* @example
|
|
1659
1768
|
* ```ts
|
|
@@ -1663,11 +1772,28 @@ export class TestHost {
|
|
|
1663
1772
|
*/
|
|
1664
1773
|
async press(binding, options = {}) {
|
|
1665
1774
|
const claims = this.#claims();
|
|
1666
|
-
const
|
|
1667
|
-
claims.get(`
|
|
1668
|
-
|
|
1775
|
+
const ids = [
|
|
1776
|
+
...(options.panel ? (claims.get(`panel ${binding}`) ?? []) : []),
|
|
1777
|
+
...(claims.get(`global ${binding}`) ?? []),
|
|
1778
|
+
];
|
|
1779
|
+
if (!ids.length)
|
|
1669
1780
|
throw new Error(`test host: nothing is bound to ${binding}`);
|
|
1670
|
-
|
|
1781
|
+
const command = (id) => this.coreCommands.get(id) ?? this.command(id);
|
|
1782
|
+
const enabled = ids.find((id) => command(id).state.enabled);
|
|
1783
|
+
if (enabled !== undefined) {
|
|
1784
|
+
await command(enabled).run();
|
|
1785
|
+
return "ran";
|
|
1786
|
+
}
|
|
1787
|
+
for (const id of ids) {
|
|
1788
|
+
const reason = this.commands.has(id)
|
|
1789
|
+
? command(id).display.reason
|
|
1790
|
+
: command(id).state.reason;
|
|
1791
|
+
if (typeof reason === "string" && reason !== "") {
|
|
1792
|
+
this.osd.push(reason);
|
|
1793
|
+
return "reason";
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return "passed";
|
|
1671
1797
|
}
|
|
1672
1798
|
/**
|
|
1673
1799
|
* Bindings declared by more than one command, each as
|
|
@@ -1789,9 +1915,12 @@ export class TestHost {
|
|
|
1789
1915
|
return {
|
|
1790
1916
|
...this.#split(key),
|
|
1791
1917
|
// What the user sees, like the player's binding table.
|
|
1792
|
-
title: shown?.display.title ?? declaration.title,
|
|
1918
|
+
title: shown?.display.title ?? String(declaration.title),
|
|
1793
1919
|
path: declaration.path?.length
|
|
1794
|
-
? (shown?.display.path ?? declaration.path)
|
|
1920
|
+
? (shown?.display.path ?? declaration.path.map(String))
|
|
1921
|
+
: null,
|
|
1922
|
+
key: declaration.path?.length
|
|
1923
|
+
? menuKeys(declaration.path.map((segment) => typeof segment === "string" ? segment : textKey(segment)))
|
|
1795
1924
|
: null,
|
|
1796
1925
|
scope: scope,
|
|
1797
1926
|
defaults: declaration.bindings,
|
|
@@ -2127,8 +2256,12 @@ export class TestHost {
|
|
|
2127
2256
|
});
|
|
2128
2257
|
throw new HostError("resolution", message);
|
|
2129
2258
|
};
|
|
2130
|
-
|
|
2131
|
-
|
|
2259
|
+
// A text value (`plugin.i18n.error`) is shown translated, like the player.
|
|
2260
|
+
const error = isTextValue(p.error)
|
|
2261
|
+
? this.#declared(p.error, "error")
|
|
2262
|
+
: p.error;
|
|
2263
|
+
if (typeof error === "string")
|
|
2264
|
+
fail(error.trim() ? [...error].slice(0, 2048).join("") : "resolver failed");
|
|
2132
2265
|
const result = (p.result ?? {});
|
|
2133
2266
|
const producer = this.producers.get(Number(result.producerId));
|
|
2134
2267
|
if (result.kind !== "stream")
|
|
@@ -2232,10 +2365,15 @@ export class TestHost {
|
|
|
2232
2365
|
throw new HostError("limit", "command registry limit reached");
|
|
2233
2366
|
const host = this;
|
|
2234
2367
|
const declaration = p.declaration;
|
|
2368
|
+
// Like the player: the menu texts and the reason are checked (and
|
|
2369
|
+
// shown) when they arrive.
|
|
2370
|
+
for (const segment of [declaration.title, ...(declaration.path ?? [])])
|
|
2371
|
+
this.#declared(segment, "a menu text");
|
|
2372
|
+
this.#checkReason(p.state);
|
|
2235
2373
|
this.commands.set(id, {
|
|
2236
2374
|
declaration,
|
|
2237
2375
|
get display() {
|
|
2238
|
-
return host.#display(declaration.path ?? [], declaration.title);
|
|
2376
|
+
return host.#display(declaration.path ?? [], declaration.title, this.state.reason);
|
|
2239
2377
|
},
|
|
2240
2378
|
state: p.state,
|
|
2241
2379
|
revision: Number(p.revision),
|
|
@@ -2256,6 +2394,7 @@ export class TestHost {
|
|
|
2256
2394
|
const command = this.command(String(p.id));
|
|
2257
2395
|
if (Number(p.revision) <= command.revision)
|
|
2258
2396
|
throw new HostError("command", `stale command revision ${p.revision}`);
|
|
2397
|
+
this.#checkReason(p.state);
|
|
2259
2398
|
command.revision = Number(p.revision);
|
|
2260
2399
|
command.state = p.state;
|
|
2261
2400
|
return { revision: command.revision };
|
|
@@ -2264,12 +2403,14 @@ export class TestHost {
|
|
|
2264
2403
|
const id = String(p.id);
|
|
2265
2404
|
if (this.windows.has(id))
|
|
2266
2405
|
throw new HostError("conflict", "view already exists");
|
|
2267
|
-
this.#
|
|
2406
|
+
const properties = this.#shownValues(p.properties);
|
|
2407
|
+
this.#checkValues(properties);
|
|
2408
|
+
this.#seeImages(properties);
|
|
2268
2409
|
const host = this;
|
|
2269
2410
|
this.windows.set(id, {
|
|
2270
2411
|
file: String(p.file),
|
|
2271
2412
|
visible: true,
|
|
2272
|
-
props: { ...
|
|
2413
|
+
props: { ...properties },
|
|
2273
2414
|
callbacks: p.callbacks,
|
|
2274
2415
|
userClose() {
|
|
2275
2416
|
host.windows.delete(id);
|
|
@@ -2287,8 +2428,10 @@ export class TestHost {
|
|
|
2287
2428
|
const window = this.windows.get(String(p.id));
|
|
2288
2429
|
if (!window)
|
|
2289
2430
|
throw new HostError("missing", "view not found");
|
|
2290
|
-
this.#
|
|
2291
|
-
|
|
2431
|
+
const value = this.#shownValues(p.value);
|
|
2432
|
+
this.#checkValues({ [String(p.property)]: value });
|
|
2433
|
+
this.#seeImages([value]);
|
|
2434
|
+
window.props[String(p.property)] = value;
|
|
2292
2435
|
},
|
|
2293
2436
|
"ui.show": (p) => {
|
|
2294
2437
|
const window = this.windows.get(String(p.id));
|
|
@@ -2320,14 +2463,15 @@ export class TestHost {
|
|
|
2320
2463
|
throw taken(this.#hostText("The sidebar holds at most {} tabs", [
|
|
2321
2464
|
MAX_SIDEBAR_TABS,
|
|
2322
2465
|
]));
|
|
2323
|
-
this.#
|
|
2466
|
+
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
2467
|
+
this.#seeImages(properties);
|
|
2324
2468
|
const host = this;
|
|
2325
2469
|
this.panel = {
|
|
2326
2470
|
slot: String(p.slot),
|
|
2327
2471
|
file: String(p.file),
|
|
2328
2472
|
component: String(p.component),
|
|
2329
2473
|
visible: true,
|
|
2330
|
-
props: { ...
|
|
2474
|
+
props: { ...properties },
|
|
2331
2475
|
locks: null,
|
|
2332
2476
|
emit(callback, ...args) {
|
|
2333
2477
|
host.emit("ui.callback", {
|
|
@@ -2400,15 +2544,21 @@ export class TestHost {
|
|
|
2400
2544
|
state.detached = p.detached;
|
|
2401
2545
|
},
|
|
2402
2546
|
"osd.show": (p) => {
|
|
2403
|
-
const text = this
|
|
2547
|
+
const text = this.#shown(p.text, p.args, "text").replace(/[\n\r]/g, " ");
|
|
2404
2548
|
if (!text.trim() || [...text].length > 200)
|
|
2405
2549
|
throw new HostError("invalid", "osd text needs 1..200 characters");
|
|
2406
2550
|
this.osd.push(text);
|
|
2407
2551
|
},
|
|
2408
|
-
"osd.panel": (p) =>
|
|
2409
|
-
p.text === null
|
|
2410
|
-
|
|
2411
|
-
|
|
2552
|
+
"osd.panel": (p) => {
|
|
2553
|
+
if (p.text === null)
|
|
2554
|
+
return void (this.osdPanel = null);
|
|
2555
|
+
const max = p.maxColumns;
|
|
2556
|
+
if (max !== undefined &&
|
|
2557
|
+
!(Number.isInteger(max) && Number(max) >= 8 && Number(max) <= 1000))
|
|
2558
|
+
throw new HostError("invalid", "maxColumns is an integer from 8 to 1000");
|
|
2559
|
+
const text = this.#shown(p.text, p.args, "text");
|
|
2560
|
+
this.osdPanel = max === undefined ? text : fitColumns(text, Number(max));
|
|
2561
|
+
},
|
|
2412
2562
|
"playback.stats": () => this.stats,
|
|
2413
2563
|
"audio.addTrack": (p) => {
|
|
2414
2564
|
this.#audioCondition(p.expected);
|
|
@@ -2541,8 +2691,11 @@ export class TestHost {
|
|
|
2541
2691
|
this.emit("sequence.step", { direction });
|
|
2542
2692
|
},
|
|
2543
2693
|
"ui.setMount": (p) => {
|
|
2544
|
-
this
|
|
2545
|
-
|
|
2694
|
+
this.mounted();
|
|
2695
|
+
const value = this.#shownValues(p.value);
|
|
2696
|
+
this.#checkValues({ [String(p.property)]: value });
|
|
2697
|
+
this.#seeImages([value]);
|
|
2698
|
+
this.mounted().props[String(p.property)] = value;
|
|
2546
2699
|
},
|
|
2547
2700
|
"timeline.marks": () => ({ marks: this.timeline.marks }),
|
|
2548
2701
|
"drops.provide": (p) => {
|
|
@@ -2579,7 +2732,8 @@ export class TestHost {
|
|
|
2579
2732
|
throw new HostError("invalid", `page ${id} already exists`);
|
|
2580
2733
|
if (this.settings.pages.size >= 8)
|
|
2581
2734
|
throw new HostError("invalid", "a plugin has at most 8 settings pages");
|
|
2582
|
-
this.#
|
|
2735
|
+
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
2736
|
+
this.#seeImages(properties);
|
|
2583
2737
|
const host = this;
|
|
2584
2738
|
this.settings.pages.set(id, {
|
|
2585
2739
|
category: String(p.category),
|
|
@@ -2588,7 +2742,7 @@ export class TestHost {
|
|
|
2588
2742
|
file: String(p.file),
|
|
2589
2743
|
component: String(p.component),
|
|
2590
2744
|
global: String(p.global),
|
|
2591
|
-
props: { ...
|
|
2745
|
+
props: { ...properties },
|
|
2592
2746
|
emit(callback, ...args) {
|
|
2593
2747
|
host.emit("ui.callback", {
|
|
2594
2748
|
view: `$settings:${id}`,
|
|
@@ -2602,8 +2756,10 @@ export class TestHost {
|
|
|
2602
2756
|
const page = this.settings.pages.get(String(p.id));
|
|
2603
2757
|
if (!page)
|
|
2604
2758
|
throw new HostError("missing", "page not registered");
|
|
2605
|
-
this.#
|
|
2606
|
-
|
|
2759
|
+
const value = this.#shownValues(p.value);
|
|
2760
|
+
this.#checkValues({ [String(p.property)]: value });
|
|
2761
|
+
this.#seeImages([value]);
|
|
2762
|
+
page.props[String(p.property)] = value;
|
|
2607
2763
|
},
|
|
2608
2764
|
"settings.unregister": (p) => {
|
|
2609
2765
|
this.settings.pages.delete(String(p.id));
|
|
@@ -2667,10 +2823,11 @@ export class TestHost {
|
|
|
2667
2823
|
if (p.sourceId !== undefined &&
|
|
2668
2824
|
p.sourceId !== this.source.stamp.generation)
|
|
2669
2825
|
return { applied: false };
|
|
2670
|
-
const
|
|
2826
|
+
const sent = p.marks;
|
|
2671
2827
|
const duration = this.source.duration;
|
|
2672
|
-
if (!Array.isArray(
|
|
2828
|
+
if (!Array.isArray(sent) || sent.length > 1000)
|
|
2673
2829
|
throw new HostError("invalid", "a layer holds at most 1000 marks");
|
|
2830
|
+
const marks = sent.map((mark) => ({ ...mark }));
|
|
2674
2831
|
for (const mark of marks) {
|
|
2675
2832
|
const time = mark.time;
|
|
2676
2833
|
if (typeof time !== "number" ||
|
|
@@ -2680,6 +2837,9 @@ export class TestHost {
|
|
|
2680
2837
|
throw new HostError("invalid", `time ${time} is outside the media`);
|
|
2681
2838
|
if (!["chapter", "bookmark", "marker"].includes(String(mark.kind)))
|
|
2682
2839
|
throw new HostError("invalid", "kind is chapter, bookmark or marker");
|
|
2840
|
+
// A text value is shown translated (`plugin.i18n.text`).
|
|
2841
|
+
if (isTextValue(mark.title))
|
|
2842
|
+
mark.title = this.#declared(mark.title, "title");
|
|
2683
2843
|
if (mark.title !== undefined &&
|
|
2684
2844
|
mark.title !== null &&
|
|
2685
2845
|
(typeof mark.title !== "string" || [...mark.title].length > 100))
|
|
@@ -2695,10 +2855,9 @@ export class TestHost {
|
|
|
2695
2855
|
"ui.interaction": (p) => void (this.mounted().locks = p),
|
|
2696
2856
|
"dialogs.confirm": (p) => {
|
|
2697
2857
|
this.confirms.push(p);
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
: undefined;
|
|
2858
|
+
const shown = (field, formatted = false) => p[field] === undefined
|
|
2859
|
+
? undefined
|
|
2860
|
+
: this.#shown(p[field], formatted && typeof p[field] === "string" ? p.args : undefined, field);
|
|
2702
2861
|
this.confirmTexts.push({
|
|
2703
2862
|
message: shown("message", true),
|
|
2704
2863
|
...(p.detail === undefined ? {} : { detail: shown("detail", true) }),
|
|
@@ -2769,7 +2928,20 @@ export class TestHost {
|
|
|
2769
2928
|
loop: loop && { a: loop.a, b: loop.b, count: loop.count ?? null },
|
|
2770
2929
|
});
|
|
2771
2930
|
},
|
|
2772
|
-
"dialogs.open": () => {
|
|
2931
|
+
"dialogs.open": (p) => {
|
|
2932
|
+
const title = p.title === undefined ? undefined : this.#declared(p.title, "title");
|
|
2933
|
+
// A string file name is data; a text is shown translated, a path
|
|
2934
|
+
// separator in the translation replaced.
|
|
2935
|
+
const fileName = p.fileName === undefined
|
|
2936
|
+
? undefined
|
|
2937
|
+
: typeof p.fileName === "string"
|
|
2938
|
+
? p.fileName
|
|
2939
|
+
: this.#declared(p.fileName, "fileName").replace(/[/\\]/g, "-");
|
|
2940
|
+
this.fileDialogs.push({
|
|
2941
|
+
kind: String(p.kind),
|
|
2942
|
+
...(title === undefined ? {} : { title }),
|
|
2943
|
+
...(fileName === undefined ? {} : { fileName }),
|
|
2944
|
+
});
|
|
2773
2945
|
const answer = this.dialogAnswers.shift();
|
|
2774
2946
|
if (answer === undefined)
|
|
2775
2947
|
throw new Error("test host: no dialog answer queued");
|
|
@@ -3098,7 +3270,11 @@ export class TestHost {
|
|
|
3098
3270
|
: p.select === "secondary"
|
|
3099
3271
|
? "secondary"
|
|
3100
3272
|
: "primary";
|
|
3101
|
-
const title =
|
|
3273
|
+
const title = isTextValue(p.title)
|
|
3274
|
+
? this.#declared(p.title, "title")
|
|
3275
|
+
: typeof p.title === "string"
|
|
3276
|
+
? p.title
|
|
3277
|
+
: null;
|
|
3102
3278
|
if (title !== null && ([...title].length < 1 || [...title].length > 200))
|
|
3103
3279
|
throw new HostError("invalid", "subtitle title needs 1..200 characters");
|
|
3104
3280
|
const bytes = Buffer.concat(upload.bytes);
|
package/dist/plugin/timeline.js
CHANGED
|
@@ -104,7 +104,9 @@ class Layer {
|
|
|
104
104
|
marks: marks.map((mark) => ({
|
|
105
105
|
time: mark?.time,
|
|
106
106
|
kind: mark?.kind,
|
|
107
|
-
...(mark?.title === undefined
|
|
107
|
+
...(mark?.title === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { title: mark.title }),
|
|
108
110
|
})),
|
|
109
111
|
};
|
|
110
112
|
if (options.ifCurrent)
|