@vestra-player/sdk 1.5.8 → 1.5.11
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 +39 -0
- package/README.md +4 -1
- package/bin/vestra-plugin.mjs +170 -7
- package/dist/commands.d.ts +7 -4
- package/dist/i18n-tools.d.ts +67 -0
- package/dist/i18n-tools.js +787 -0
- package/dist/i18n.d.ts +222 -0
- package/dist/i18n.js +972 -0
- package/dist/player-runtime.d.ts +2 -0
- package/dist/player-runtime.js +2 -0
- package/dist/plugin/appearance.js +27 -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 +8 -0
- package/dist/plugin/language.js +79 -0
- package/dist/plugin/osd.d.ts +11 -0
- package/dist/plugin/osd.js +64 -9
- package/dist/plugin/packages.js +10 -0
- package/dist/plugin/runtime.d.ts +3 -1
- package/dist/plugin/runtime.js +6 -0
- package/dist/plugin/scope.d.ts +1 -0
- package/dist/plugin/scope.js +2 -0
- package/dist/plugin/subtitles.js +9 -1
- package/dist/plugin/testing.d.ts +184 -5
- package/dist/plugin/testing.js +459 -67
- package/dist/plugin/timeline.js +3 -1
- package/dist/plugin/types.d.ts +523 -54
- package/dist/plugin/ui.js +29 -10
- package/dist/plugin/values.d.ts +55 -0
- package/dist/plugin/values.js +117 -0
- package/dist/plugin/window.d.ts +2 -0
- package/dist/plugin/window.js +4 -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 +38 -6
- package/dist/wire.js +13 -2
- package/llms-full.txt +646 -58
- package/llms.txt +44 -40
- package/package.json +1 -1
- package/slint/vestra/markdown.slint +5 -5
- package/slint/vestra/widgets.slint +4 -4
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,14 +66,50 @@ 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";
|
|
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";
|
|
72
75
|
/** Throw one of these from `host.handle(...)` to simulate a definite refusal. */
|
|
73
76
|
export { HostError };
|
|
74
77
|
/** The player's command caps, which the test host enforces as well. */
|
|
75
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 };
|
|
97
|
+
/**
|
|
98
|
+
* The player's interface languages, English (the source) first, as the
|
|
99
|
+
* player's own table: `["en", "zh-Hans", "ja", "ko", "de", "fr", "es"]`.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* import { LOCALES } from "@vestra-player/sdk/testing";
|
|
104
|
+
*
|
|
105
|
+
* for (const locale of LOCALES.slice(1)) {
|
|
106
|
+
* const { host, stop } = await startPlugin(activate, { directory, locale });
|
|
107
|
+
* assert.notEqual(host.command("open").display.title, "Open…");
|
|
108
|
+
* await stop();
|
|
109
|
+
* }
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export { LOCALES };
|
|
76
113
|
const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
77
114
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
78
115
|
function initialSource() {
|
|
@@ -121,9 +158,15 @@ const taken = (message) => {
|
|
|
121
158
|
return error;
|
|
122
159
|
};
|
|
123
160
|
const SLOT_LABELS = {
|
|
124
|
-
bottom: "
|
|
125
|
-
sidebar: "
|
|
126
|
-
backdrop: "
|
|
161
|
+
bottom: "bottom bar",
|
|
162
|
+
sidebar: "sidebar",
|
|
163
|
+
backdrop: "video backdrop",
|
|
164
|
+
};
|
|
165
|
+
/** Texts only the test host uses, with their Chinese of SDK 1.5.8. */
|
|
166
|
+
const TEST_ZH_HANS = {
|
|
167
|
+
"this site": "这个网站",
|
|
168
|
+
"External SRT": "外部 SRT",
|
|
169
|
+
"External ASS": "外部 ASS",
|
|
127
170
|
};
|
|
128
171
|
/** The player's limit of sidebar tabs. */
|
|
129
172
|
const MAX_SIDEBAR_TABS = 8;
|
|
@@ -227,10 +270,177 @@ export class TestHost {
|
|
|
227
270
|
bottom: null,
|
|
228
271
|
sidebar: null,
|
|
229
272
|
};
|
|
230
|
-
/**
|
|
273
|
+
/**
|
|
274
|
+
* Every `osd.show` text as the player shows it (translated with the
|
|
275
|
+
* plugin's catalog and formatted with its args), newest last. The
|
|
276
|
+
* template and args the plugin sent are in `host.sent("osd.show")`.
|
|
277
|
+
*/
|
|
231
278
|
osd = [];
|
|
232
|
-
/** The text of this plugin's osd panel, `null` when hidden. */
|
|
279
|
+
/** The text of this plugin's osd panel as shown, `null` when hidden. */
|
|
233
280
|
osdPanel = null;
|
|
281
|
+
#locale;
|
|
282
|
+
/**
|
|
283
|
+
* The interface language the test host plays (`plugin.i18n.locale`), `"en"`
|
|
284
|
+
* by default. Set it before {@link startPlugin} (or pass its `locale`
|
|
285
|
+
* option): the plugin's catalog for it is read from the package's
|
|
286
|
+
* `locales/` then, and the menus ({@link FakeCommand.display}), OSD
|
|
287
|
+
* messages, dialog texts, settings page and panel titles show its
|
|
288
|
+
* translations, the way the player shows them.
|
|
289
|
+
*
|
|
290
|
+
* The player's own texts (core actions' messages and menus, its
|
|
291
|
+
* refusals) are English, or the player's Simplified Chinese for
|
|
292
|
+
* `"zh-Hans"`. While `locale` was never set they stay in Chinese, as
|
|
293
|
+
* players up to 0.4.2 showed them, so tests written for SDK 1.5.8 keep
|
|
294
|
+
* passing; set `locale` to test what a user sees now.
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```ts
|
|
298
|
+
* const { host, stop } = await startPlugin(activate, { directory, locale: "zh-Hans" });
|
|
299
|
+
* assert.deepEqual(host.command("blur").display.path, ["视频", "模糊…"]);
|
|
300
|
+
* await stop();
|
|
301
|
+
* ```
|
|
302
|
+
*
|
|
303
|
+
* @since 1.5.9
|
|
304
|
+
*/
|
|
305
|
+
get locale() {
|
|
306
|
+
return this.#locale ?? "en";
|
|
307
|
+
}
|
|
308
|
+
set locale(locale) {
|
|
309
|
+
if (!LOCALES.includes(locale))
|
|
310
|
+
throw new Error(`test host: unknown locale ${locale}`);
|
|
311
|
+
this.#locale = locale;
|
|
312
|
+
}
|
|
313
|
+
/** The plugin's translations for `locale` (see `loadCatalog`). */
|
|
314
|
+
#catalog = new Catalog();
|
|
315
|
+
/**
|
|
316
|
+
* Read the plugin's translations for {@link TestHost.locale} from a
|
|
317
|
+
* package directory, as the player would (`locales/<locale>.json` of a
|
|
318
|
+
* built package, else the source `locales/<locale>.po`); `startPlugin`
|
|
319
|
+
* does this. None for English; a missing file means none; a broken `.po`
|
|
320
|
+
* throws.
|
|
321
|
+
*
|
|
322
|
+
* @param directory - The package directory (holding `locales/`).
|
|
323
|
+
* @since 1.5.9
|
|
324
|
+
*/
|
|
325
|
+
loadCatalog(directory) {
|
|
326
|
+
this.#catalog = loadPackageCatalog(directory, this.locale);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* A text of the plugin as the player shows it: translated with the
|
|
330
|
+
* plugin's catalog ({@link TestHost.loadCatalog}), then formatted with
|
|
331
|
+
* `args` when given.
|
|
332
|
+
*
|
|
333
|
+
* @param text - English source text or template.
|
|
334
|
+
* @param args - Values for its placeholders.
|
|
335
|
+
* @returns The shown text.
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```ts
|
|
339
|
+
* assert.equal(host.translate("Speed {}×", [2]), "倍速 2×");
|
|
340
|
+
* ```
|
|
341
|
+
*
|
|
342
|
+
* @since 1.5.9
|
|
343
|
+
*/
|
|
344
|
+
translate(text, args) {
|
|
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;
|
|
382
|
+
}
|
|
383
|
+
/** A text of the player itself, in the player's language (see `locale`). */
|
|
384
|
+
#hostText(text, args) {
|
|
385
|
+
const zh = this.#locale === undefined || this.#locale === "zh-Hans";
|
|
386
|
+
const shown = (zh && (HOST_ZH_HANS[text] ?? TEST_ZH_HANS[text])) || text;
|
|
387
|
+
return args === undefined
|
|
388
|
+
? shown
|
|
389
|
+
: formatTemplate(shown, args);
|
|
390
|
+
}
|
|
391
|
+
/** A declared menu path and title as the player shows them. */
|
|
392
|
+
#display(path, title, reason = null) {
|
|
393
|
+
const keys = menuKeys(path.map((segment) => typeof segment === "string" ? segment : textKey(segment)));
|
|
394
|
+
const display = {
|
|
395
|
+
title: this.translate(title),
|
|
396
|
+
// The player's submenus by key, everything else as declared.
|
|
397
|
+
path: keys.map((segment, index) => index < keys.length - 1 &&
|
|
398
|
+
HOST_GROUPS.includes(keys.slice(0, index + 1).join("/"))
|
|
399
|
+
? this.#hostText(segment)
|
|
400
|
+
: this.translate((path[index] ?? segment))),
|
|
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;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* The saved interface language (`plugin.appearance.language`), changed by
|
|
411
|
+
* `appearance.setLanguage` or {@link TestHost.setLanguage}.
|
|
412
|
+
*
|
|
413
|
+
* @since 1.5.9
|
|
414
|
+
*/
|
|
415
|
+
language = "system";
|
|
416
|
+
/**
|
|
417
|
+
* Simulate another plugin (or the user) saving the interface language:
|
|
418
|
+
* pushes `appearance.changed`.
|
|
419
|
+
*
|
|
420
|
+
* @param language - `"system"` or a locale.
|
|
421
|
+
* @since 1.5.9
|
|
422
|
+
*/
|
|
423
|
+
setLanguage(language) {
|
|
424
|
+
this.language = language;
|
|
425
|
+
this.emit("appearance.changed", { theme: this.appearance, language });
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Every `dialogs.confirm` as the player shows it: its texts translated
|
|
429
|
+
* (message and detail formatted with `args`), newest last. The options
|
|
430
|
+
* the plugin sent are in {@link TestHost.confirms}.
|
|
431
|
+
*
|
|
432
|
+
* @since 1.5.9
|
|
433
|
+
*/
|
|
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 = [];
|
|
234
444
|
/** The sequence provider's flags, `null` without a provider. */
|
|
235
445
|
sequence = null;
|
|
236
446
|
/**
|
|
@@ -432,6 +642,42 @@ export class TestHost {
|
|
|
432
642
|
trustedExecution: true,
|
|
433
643
|
};
|
|
434
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
|
+
}
|
|
435
681
|
#seeImages(values) {
|
|
436
682
|
for (const value of Object.values(values)) {
|
|
437
683
|
const path = this.imagePath(value);
|
|
@@ -1013,14 +1259,14 @@ export class TestHost {
|
|
|
1013
1259
|
/** Why one frame that way is impossible (B07), or null; the player's rules. */
|
|
1014
1260
|
#frameCheck(forward) {
|
|
1015
1261
|
const s = this.source;
|
|
1016
|
-
const unsupported = "
|
|
1017
|
-
const noBack = "
|
|
1262
|
+
const unsupported = this.#hostText("Not supported by the current source");
|
|
1263
|
+
const noBack = this.#hostText("The current source cannot step back a frame");
|
|
1018
1264
|
if (s.phase !== "ready" && s.phase !== "ended")
|
|
1019
1265
|
return unsupported;
|
|
1020
1266
|
if (s.kind !== "media")
|
|
1021
1267
|
return forward ? unsupported : noBack;
|
|
1022
1268
|
if (s.hasVideo === false)
|
|
1023
|
-
return "
|
|
1269
|
+
return this.#hostText("No video");
|
|
1024
1270
|
if (forward && s.phase !== "ready")
|
|
1025
1271
|
return unsupported;
|
|
1026
1272
|
if (!forward && !s.capabilities.seek)
|
|
@@ -1031,12 +1277,16 @@ export class TestHost {
|
|
|
1031
1277
|
#coreState(action) {
|
|
1032
1278
|
const f = this.#facts();
|
|
1033
1279
|
const s = this.source;
|
|
1034
|
-
const unsupported = "
|
|
1280
|
+
const unsupported = this.#hostText("Not supported by the current source");
|
|
1035
1281
|
const rule = {
|
|
1036
1282
|
"play-pause": [f.toggle, f.ready ? s.paused : null, unsupported],
|
|
1037
1283
|
stop: [f.close, null, unsupported],
|
|
1038
|
-
previous: [
|
|
1039
|
-
|
|
1284
|
+
previous: [
|
|
1285
|
+
!!this.sequence?.canPrevious,
|
|
1286
|
+
null,
|
|
1287
|
+
this.#hostText("No previous item"),
|
|
1288
|
+
],
|
|
1289
|
+
next: [!!this.sequence?.canNext, null, this.#hostText("No next item")],
|
|
1040
1290
|
"frame-back": [
|
|
1041
1291
|
!this.#frameCheck(false),
|
|
1042
1292
|
null,
|
|
@@ -1061,11 +1311,18 @@ export class TestHost {
|
|
|
1061
1311
|
}
|
|
1062
1312
|
#registerCoreActions() {
|
|
1063
1313
|
const table = [
|
|
1064
|
-
["stop", ["
|
|
1065
|
-
[
|
|
1314
|
+
["stop", ["Close"], 120, "open", ["F4"], false],
|
|
1315
|
+
[
|
|
1316
|
+
"play-pause",
|
|
1317
|
+
["Playback", "Play/Pause"],
|
|
1318
|
+
201,
|
|
1319
|
+
"domain",
|
|
1320
|
+
["Space"],
|
|
1321
|
+
false,
|
|
1322
|
+
],
|
|
1066
1323
|
[
|
|
1067
1324
|
"previous",
|
|
1068
|
-
["
|
|
1325
|
+
["Playback", "Previous"],
|
|
1069
1326
|
202,
|
|
1070
1327
|
"domain",
|
|
1071
1328
|
["PageUp", "Mouse:X1"],
|
|
@@ -1073,17 +1330,31 @@ export class TestHost {
|
|
|
1073
1330
|
],
|
|
1074
1331
|
[
|
|
1075
1332
|
"next",
|
|
1076
|
-
["
|
|
1333
|
+
["Playback", "Next"],
|
|
1077
1334
|
203,
|
|
1078
1335
|
"domain",
|
|
1079
1336
|
["PageDown", "Mouse:X2"],
|
|
1080
1337
|
false,
|
|
1081
1338
|
],
|
|
1082
|
-
[
|
|
1083
|
-
|
|
1339
|
+
[
|
|
1340
|
+
"frame-back",
|
|
1341
|
+
["Playback", "Seek", "Previous Frame"],
|
|
1342
|
+
204,
|
|
1343
|
+
"frame",
|
|
1344
|
+
["D"],
|
|
1345
|
+
true,
|
|
1346
|
+
],
|
|
1347
|
+
[
|
|
1348
|
+
"frame-forward",
|
|
1349
|
+
["Playback", "Seek", "Next Frame"],
|
|
1350
|
+
205,
|
|
1351
|
+
"frame",
|
|
1352
|
+
["F"],
|
|
1353
|
+
true,
|
|
1354
|
+
],
|
|
1084
1355
|
[
|
|
1085
1356
|
"seek-backward",
|
|
1086
|
-
["
|
|
1357
|
+
["Playback", "Seek", "Back 5 s"],
|
|
1087
1358
|
210,
|
|
1088
1359
|
"step",
|
|
1089
1360
|
["ArrowLeft"],
|
|
@@ -1091,7 +1362,7 @@ export class TestHost {
|
|
|
1091
1362
|
],
|
|
1092
1363
|
[
|
|
1093
1364
|
"seek-forward",
|
|
1094
|
-
["
|
|
1365
|
+
["Playback", "Seek", "Forward 5 s"],
|
|
1095
1366
|
211,
|
|
1096
1367
|
"step",
|
|
1097
1368
|
["ArrowRight"],
|
|
@@ -1099,18 +1370,32 @@ export class TestHost {
|
|
|
1099
1370
|
],
|
|
1100
1371
|
[
|
|
1101
1372
|
"rate-normal",
|
|
1102
|
-
["
|
|
1373
|
+
["Playback", "Speed", "Normal"],
|
|
1103
1374
|
220,
|
|
1104
1375
|
"domain",
|
|
1105
1376
|
["Z"],
|
|
1106
1377
|
false,
|
|
1107
1378
|
true,
|
|
1108
1379
|
],
|
|
1109
|
-
[
|
|
1110
|
-
|
|
1380
|
+
[
|
|
1381
|
+
"rate-down",
|
|
1382
|
+
["Playback", "Speed", "Slower 0.1"],
|
|
1383
|
+
221,
|
|
1384
|
+
"domain",
|
|
1385
|
+
["X"],
|
|
1386
|
+
false,
|
|
1387
|
+
],
|
|
1388
|
+
[
|
|
1389
|
+
"rate-up",
|
|
1390
|
+
["Playback", "Speed", "Faster 0.1"],
|
|
1391
|
+
222,
|
|
1392
|
+
"domain",
|
|
1393
|
+
["C"],
|
|
1394
|
+
false,
|
|
1395
|
+
],
|
|
1111
1396
|
[
|
|
1112
1397
|
"volume-up",
|
|
1113
|
-
["
|
|
1398
|
+
["Audio", "Volume +"],
|
|
1114
1399
|
501,
|
|
1115
1400
|
"domain",
|
|
1116
1401
|
["ArrowUp", "Mouse:WheelUp"],
|
|
@@ -1118,13 +1403,13 @@ export class TestHost {
|
|
|
1118
1403
|
],
|
|
1119
1404
|
[
|
|
1120
1405
|
"volume-down",
|
|
1121
|
-
["
|
|
1406
|
+
["Audio", "Volume −"],
|
|
1122
1407
|
502,
|
|
1123
1408
|
"domain",
|
|
1124
1409
|
["ArrowDown", "Mouse:WheelDown"],
|
|
1125
1410
|
true,
|
|
1126
1411
|
],
|
|
1127
|
-
["mute", ["
|
|
1412
|
+
["mute", ["Audio", "Mute"], 503, "domain", ["M"], false],
|
|
1128
1413
|
];
|
|
1129
1414
|
const host = this;
|
|
1130
1415
|
for (const [id, path, order, group, bindings, repeat, radio] of table) {
|
|
@@ -1138,6 +1423,18 @@ export class TestHost {
|
|
|
1138
1423
|
bindings,
|
|
1139
1424
|
repeat,
|
|
1140
1425
|
},
|
|
1426
|
+
// The player's own words in the player's language.
|
|
1427
|
+
get display() {
|
|
1428
|
+
const leaf = path.at(-1);
|
|
1429
|
+
const step = /^(Back|Forward) (\S+) s$/.exec(leaf);
|
|
1430
|
+
const title = step
|
|
1431
|
+
? host.#hostText(`${step[1]} {} s`, [step[2]])
|
|
1432
|
+
: host.#hostText(leaf);
|
|
1433
|
+
return {
|
|
1434
|
+
title,
|
|
1435
|
+
path: [...path.slice(0, -1).map((s) => host.#hostText(s)), title],
|
|
1436
|
+
};
|
|
1437
|
+
},
|
|
1141
1438
|
get state() {
|
|
1142
1439
|
return host.#coreState(id);
|
|
1143
1440
|
},
|
|
@@ -1175,7 +1472,7 @@ export class TestHost {
|
|
|
1175
1472
|
kind: "control",
|
|
1176
1473
|
control: { kind: "pause", paused: !s.paused },
|
|
1177
1474
|
});
|
|
1178
|
-
this.osd.push(s.phase !== "ended" && !s.paused ? "
|
|
1475
|
+
this.osd.push(this.#hostText(s.phase !== "ended" && !s.paused ? "Paused" : "Playing"));
|
|
1179
1476
|
return;
|
|
1180
1477
|
case "stop":
|
|
1181
1478
|
this.#intent({ kind: "close" });
|
|
@@ -1200,7 +1497,7 @@ export class TestHost {
|
|
|
1200
1497
|
kind: "control",
|
|
1201
1498
|
control: { kind: "seek", seconds: target },
|
|
1202
1499
|
});
|
|
1203
|
-
this.osd.push(
|
|
1500
|
+
this.osd.push(this.#hostText(delta < 0 ? "Back {} s: {} / {}" : "Forward {} s: {} / {}", [Math.abs(delta), time(target), time(s.duration)]));
|
|
1204
1501
|
return;
|
|
1205
1502
|
}
|
|
1206
1503
|
case "rate-normal":
|
|
@@ -1210,7 +1507,7 @@ export class TestHost {
|
|
|
1210
1507
|
? 1
|
|
1211
1508
|
: Math.max(0.25, Math.min(4, Math.round((s.rate + (action === "rate-up" ? 0.1 : -0.1)) * 100) / 100));
|
|
1212
1509
|
this.#intent({ kind: "control", control: { kind: "rate", rate } });
|
|
1213
|
-
this.osd.push(
|
|
1510
|
+
this.osd.push(this.#hostText("Speed: {}x", [rateText(rate)]));
|
|
1214
1511
|
return;
|
|
1215
1512
|
}
|
|
1216
1513
|
case "volume-up":
|
|
@@ -1221,12 +1518,12 @@ export class TestHost {
|
|
|
1221
1518
|
? Math.max(s.output.volume, Math.min(100, Math.round(s.output.volume + 5)))
|
|
1222
1519
|
: Math.max(0, Math.min(200, Math.round(s.output.volume - 5)));
|
|
1223
1520
|
this.setSource({ output: { volume, muted: false } });
|
|
1224
|
-
this.osd.push(
|
|
1521
|
+
this.osd.push(this.#hostText("Volume: {}%", [volume]));
|
|
1225
1522
|
return;
|
|
1226
1523
|
}
|
|
1227
1524
|
case "mute":
|
|
1228
1525
|
this.setSource({ output: { ...s.output, muted: !s.output.muted } });
|
|
1229
|
-
this.osd.push(s.output.muted ? "
|
|
1526
|
+
this.osd.push(this.#hostText(s.output.muted ? "Mute: Off" : "Mute: On"));
|
|
1230
1527
|
return;
|
|
1231
1528
|
}
|
|
1232
1529
|
}
|
|
@@ -1399,6 +1696,8 @@ export class TestHost {
|
|
|
1399
1696
|
pictureInPicture: { ...this.mainWindow.pictureInPicture },
|
|
1400
1697
|
appearance: this.appearance,
|
|
1401
1698
|
fileTypes: this.fileTypes(),
|
|
1699
|
+
language: this.language,
|
|
1700
|
+
locale: this.locale,
|
|
1402
1701
|
};
|
|
1403
1702
|
}
|
|
1404
1703
|
/**
|
|
@@ -1586,10 +1885,17 @@ export class TestHost {
|
|
|
1586
1885
|
const key = this.#bindingKey(id);
|
|
1587
1886
|
const scope = declaration.focus ? "panel" : "global";
|
|
1588
1887
|
const current = this.bindings.overlay.get(key) ?? declaration.bindings;
|
|
1888
|
+
const shown = this.commands.get(id) ?? this.coreCommands.get(id);
|
|
1589
1889
|
return {
|
|
1590
1890
|
...this.#split(key),
|
|
1591
|
-
|
|
1592
|
-
|
|
1891
|
+
// What the user sees, like the player's binding table.
|
|
1892
|
+
title: shown?.display.title ?? String(declaration.title),
|
|
1893
|
+
path: declaration.path?.length
|
|
1894
|
+
? (shown?.display.path ?? declaration.path.map(String))
|
|
1895
|
+
: null,
|
|
1896
|
+
key: declaration.path?.length
|
|
1897
|
+
? menuKeys(declaration.path.map((segment) => typeof segment === "string" ? segment : textKey(segment)))
|
|
1898
|
+
: null,
|
|
1593
1899
|
scope: scope,
|
|
1594
1900
|
defaults: declaration.bindings,
|
|
1595
1901
|
current,
|
|
@@ -1898,8 +2204,10 @@ export class TestHost {
|
|
|
1898
2204
|
for (const other of this.otherResolvers) {
|
|
1899
2205
|
const what = claims(other);
|
|
1900
2206
|
if (what) {
|
|
1901
|
-
const target = what === "host"
|
|
1902
|
-
|
|
2207
|
+
const target = what === "host"
|
|
2208
|
+
? this.#hostText("this site")
|
|
2209
|
+
: this.#hostText("File type .{}", [what]);
|
|
2210
|
+
throw taken(this.#hostText("{} is resolved by {}", [target, other.owner]));
|
|
1903
2211
|
}
|
|
1904
2212
|
}
|
|
1905
2213
|
this.resolvers.set(id, p);
|
|
@@ -1922,8 +2230,12 @@ export class TestHost {
|
|
|
1922
2230
|
});
|
|
1923
2231
|
throw new HostError("resolution", message);
|
|
1924
2232
|
};
|
|
1925
|
-
|
|
1926
|
-
|
|
2233
|
+
// A text value (`plugin.i18n.error`) is shown translated, like the player.
|
|
2234
|
+
const error = isTextValue(p.error)
|
|
2235
|
+
? this.#declared(p.error, "error")
|
|
2236
|
+
: p.error;
|
|
2237
|
+
if (typeof error === "string")
|
|
2238
|
+
fail(error.trim() ? [...error].slice(0, 2048).join("") : "resolver failed");
|
|
1927
2239
|
const result = (p.result ?? {});
|
|
1928
2240
|
const producer = this.producers.get(Number(result.producerId));
|
|
1929
2241
|
if (result.kind !== "stream")
|
|
@@ -2026,8 +2338,17 @@ export class TestHost {
|
|
|
2026
2338
|
this.commands.size + this.coreCommands.size >= COMMAND_LIMITS.total)
|
|
2027
2339
|
throw new HostError("limit", "command registry limit reached");
|
|
2028
2340
|
const host = this;
|
|
2341
|
+
const declaration = p.declaration;
|
|
2342
|
+
// Like the player: the menu texts and the reason are checked (and
|
|
2343
|
+
// shown) when they arrive.
|
|
2344
|
+
for (const segment of [declaration.title, ...(declaration.path ?? [])])
|
|
2345
|
+
this.#declared(segment, "a menu text");
|
|
2346
|
+
this.#checkReason(p.state);
|
|
2029
2347
|
this.commands.set(id, {
|
|
2030
|
-
declaration
|
|
2348
|
+
declaration,
|
|
2349
|
+
get display() {
|
|
2350
|
+
return host.#display(declaration.path ?? [], declaration.title, this.state.reason);
|
|
2351
|
+
},
|
|
2031
2352
|
state: p.state,
|
|
2032
2353
|
revision: Number(p.revision),
|
|
2033
2354
|
async run() {
|
|
@@ -2047,6 +2368,7 @@ export class TestHost {
|
|
|
2047
2368
|
const command = this.command(String(p.id));
|
|
2048
2369
|
if (Number(p.revision) <= command.revision)
|
|
2049
2370
|
throw new HostError("command", `stale command revision ${p.revision}`);
|
|
2371
|
+
this.#checkReason(p.state);
|
|
2050
2372
|
command.revision = Number(p.revision);
|
|
2051
2373
|
command.state = p.state;
|
|
2052
2374
|
return { revision: command.revision };
|
|
@@ -2055,12 +2377,14 @@ export class TestHost {
|
|
|
2055
2377
|
const id = String(p.id);
|
|
2056
2378
|
if (this.windows.has(id))
|
|
2057
2379
|
throw new HostError("conflict", "view already exists");
|
|
2058
|
-
this.#
|
|
2380
|
+
const properties = this.#shownValues(p.properties);
|
|
2381
|
+
this.#checkValues(properties);
|
|
2382
|
+
this.#seeImages(properties);
|
|
2059
2383
|
const host = this;
|
|
2060
2384
|
this.windows.set(id, {
|
|
2061
2385
|
file: String(p.file),
|
|
2062
2386
|
visible: true,
|
|
2063
|
-
props: { ...
|
|
2387
|
+
props: { ...properties },
|
|
2064
2388
|
callbacks: p.callbacks,
|
|
2065
2389
|
userClose() {
|
|
2066
2390
|
host.windows.delete(id);
|
|
@@ -2078,8 +2402,10 @@ export class TestHost {
|
|
|
2078
2402
|
const window = this.windows.get(String(p.id));
|
|
2079
2403
|
if (!window)
|
|
2080
2404
|
throw new HostError("missing", "view not found");
|
|
2081
|
-
this.#
|
|
2082
|
-
|
|
2405
|
+
const value = this.#shownValues(p.value);
|
|
2406
|
+
this.#checkValues({ [String(p.property)]: value });
|
|
2407
|
+
this.#seeImages([value]);
|
|
2408
|
+
window.props[String(p.property)] = value;
|
|
2083
2409
|
},
|
|
2084
2410
|
"ui.show": (p) => {
|
|
2085
2411
|
const window = this.windows.get(String(p.id));
|
|
@@ -2102,18 +2428,24 @@ export class TestHost {
|
|
|
2102
2428
|
? this.layout.bottom?.panels?.[0]?.plugin
|
|
2103
2429
|
: undefined;
|
|
2104
2430
|
if (holder !== undefined || (p.slot === "bottom" && this.layout.bottom))
|
|
2105
|
-
throw taken(
|
|
2431
|
+
throw taken(this.#hostText("The {} is used by {}", [
|
|
2432
|
+
this.#hostText(SLOT_LABELS[String(p.slot)]),
|
|
2433
|
+
String(holder),
|
|
2434
|
+
]));
|
|
2106
2435
|
if (p.slot === "sidebar" &&
|
|
2107
2436
|
(this.layout.sidebar?.panels?.length ?? 0) >= MAX_SIDEBAR_TABS)
|
|
2108
|
-
throw taken(
|
|
2109
|
-
|
|
2437
|
+
throw taken(this.#hostText("The sidebar holds at most {} tabs", [
|
|
2438
|
+
MAX_SIDEBAR_TABS,
|
|
2439
|
+
]));
|
|
2440
|
+
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
2441
|
+
this.#seeImages(properties);
|
|
2110
2442
|
const host = this;
|
|
2111
2443
|
this.panel = {
|
|
2112
2444
|
slot: String(p.slot),
|
|
2113
2445
|
file: String(p.file),
|
|
2114
2446
|
component: String(p.component),
|
|
2115
2447
|
visible: true,
|
|
2116
|
-
props: { ...
|
|
2448
|
+
props: { ...properties },
|
|
2117
2449
|
locks: null,
|
|
2118
2450
|
emit(callback, ...args) {
|
|
2119
2451
|
host.emit("ui.callback", {
|
|
@@ -2131,7 +2463,9 @@ export class TestHost {
|
|
|
2131
2463
|
this.#addPanel(slot, {
|
|
2132
2464
|
owner: 1,
|
|
2133
2465
|
plugin: this.manifestId,
|
|
2134
|
-
title: typeof p.title === "string" && p.title
|
|
2466
|
+
title: typeof p.title === "string" && p.title
|
|
2467
|
+
? this.translate(p.title)
|
|
2468
|
+
: this.manifestId,
|
|
2135
2469
|
});
|
|
2136
2470
|
return { status: "ready", prepareMs: 0, shellGeneration: 1 };
|
|
2137
2471
|
},
|
|
@@ -2184,12 +2518,21 @@ export class TestHost {
|
|
|
2184
2518
|
state.detached = p.detached;
|
|
2185
2519
|
},
|
|
2186
2520
|
"osd.show": (p) => {
|
|
2187
|
-
const text =
|
|
2188
|
-
if (!text || [...text].length > 200)
|
|
2521
|
+
const text = this.#shown(p.text, p.args, "text").replace(/[\n\r]/g, " ");
|
|
2522
|
+
if (!text.trim() || [...text].length > 200)
|
|
2189
2523
|
throw new HostError("invalid", "osd text needs 1..200 characters");
|
|
2190
2524
|
this.osd.push(text);
|
|
2191
2525
|
},
|
|
2192
|
-
"osd.panel": (p) =>
|
|
2526
|
+
"osd.panel": (p) => {
|
|
2527
|
+
if (p.text === null)
|
|
2528
|
+
return void (this.osdPanel = null);
|
|
2529
|
+
const max = p.maxColumns;
|
|
2530
|
+
if (max !== undefined &&
|
|
2531
|
+
!(Number.isInteger(max) && Number(max) >= 8 && Number(max) <= 1000))
|
|
2532
|
+
throw new HostError("invalid", "maxColumns is an integer from 8 to 1000");
|
|
2533
|
+
const text = this.#shown(p.text, p.args, "text");
|
|
2534
|
+
this.osdPanel = max === undefined ? text : fitColumns(text, Number(max));
|
|
2535
|
+
},
|
|
2193
2536
|
"playback.stats": () => this.stats,
|
|
2194
2537
|
"audio.addTrack": (p) => {
|
|
2195
2538
|
this.#audioCondition(p.expected);
|
|
@@ -2292,7 +2635,7 @@ export class TestHost {
|
|
|
2292
2635
|
const reason = p.unit === "keyframe"
|
|
2293
2636
|
? s.phase === "ready" && s.capabilities.seek && s.hasVideo !== false
|
|
2294
2637
|
? null
|
|
2295
|
-
: "
|
|
2638
|
+
: this.#hostText("Not supported by the current source")
|
|
2296
2639
|
: this.#frameCheck(p.direction === "forward");
|
|
2297
2640
|
if (reason)
|
|
2298
2641
|
throw new HostError("unsupported", reason);
|
|
@@ -2322,8 +2665,11 @@ export class TestHost {
|
|
|
2322
2665
|
this.emit("sequence.step", { direction });
|
|
2323
2666
|
},
|
|
2324
2667
|
"ui.setMount": (p) => {
|
|
2325
|
-
this
|
|
2326
|
-
|
|
2668
|
+
this.mounted();
|
|
2669
|
+
const value = this.#shownValues(p.value);
|
|
2670
|
+
this.#checkValues({ [String(p.property)]: value });
|
|
2671
|
+
this.#seeImages([value]);
|
|
2672
|
+
this.mounted().props[String(p.property)] = value;
|
|
2327
2673
|
},
|
|
2328
2674
|
"timeline.marks": () => ({ marks: this.timeline.marks }),
|
|
2329
2675
|
"drops.provide": (p) => {
|
|
@@ -2339,6 +2685,14 @@ export class TestHost {
|
|
|
2339
2685
|
this.dropProviders.splice(index, 1);
|
|
2340
2686
|
},
|
|
2341
2687
|
"drops.listen": (p) => void (this.panelDrops = p.listen === true),
|
|
2688
|
+
"appearance.setLanguage": (p) => {
|
|
2689
|
+
const language = String(p.language);
|
|
2690
|
+
if (language !== "system" &&
|
|
2691
|
+
!LOCALES.includes(language))
|
|
2692
|
+
throw new HostError("invalid", `unknown language ${language}`);
|
|
2693
|
+
if (language !== this.language)
|
|
2694
|
+
this.setLanguage(language);
|
|
2695
|
+
},
|
|
2342
2696
|
"appearance.set": (p) => {
|
|
2343
2697
|
// Like the player: an id it does not have selects the default (the
|
|
2344
2698
|
// SDK's own `set` refuses one before it gets here).
|
|
@@ -2352,16 +2706,17 @@ export class TestHost {
|
|
|
2352
2706
|
throw new HostError("invalid", `page ${id} already exists`);
|
|
2353
2707
|
if (this.settings.pages.size >= 8)
|
|
2354
2708
|
throw new HostError("invalid", "a plugin has at most 8 settings pages");
|
|
2355
|
-
this.#
|
|
2709
|
+
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
2710
|
+
this.#seeImages(properties);
|
|
2356
2711
|
const host = this;
|
|
2357
2712
|
this.settings.pages.set(id, {
|
|
2358
2713
|
category: String(p.category),
|
|
2359
|
-
title: String(p.title),
|
|
2714
|
+
title: this.translate(String(p.title)),
|
|
2360
2715
|
order: Number(p.order ?? 0),
|
|
2361
2716
|
file: String(p.file),
|
|
2362
2717
|
component: String(p.component),
|
|
2363
2718
|
global: String(p.global),
|
|
2364
|
-
props: { ...
|
|
2719
|
+
props: { ...properties },
|
|
2365
2720
|
emit(callback, ...args) {
|
|
2366
2721
|
host.emit("ui.callback", {
|
|
2367
2722
|
view: `$settings:${id}`,
|
|
@@ -2375,8 +2730,10 @@ export class TestHost {
|
|
|
2375
2730
|
const page = this.settings.pages.get(String(p.id));
|
|
2376
2731
|
if (!page)
|
|
2377
2732
|
throw new HostError("missing", "page not registered");
|
|
2378
|
-
this.#
|
|
2379
|
-
|
|
2733
|
+
const value = this.#shownValues(p.value);
|
|
2734
|
+
this.#checkValues({ [String(p.property)]: value });
|
|
2735
|
+
this.#seeImages([value]);
|
|
2736
|
+
page.props[String(p.property)] = value;
|
|
2380
2737
|
},
|
|
2381
2738
|
"settings.unregister": (p) => {
|
|
2382
2739
|
this.settings.pages.delete(String(p.id));
|
|
@@ -2440,10 +2797,11 @@ export class TestHost {
|
|
|
2440
2797
|
if (p.sourceId !== undefined &&
|
|
2441
2798
|
p.sourceId !== this.source.stamp.generation)
|
|
2442
2799
|
return { applied: false };
|
|
2443
|
-
const
|
|
2800
|
+
const sent = p.marks;
|
|
2444
2801
|
const duration = this.source.duration;
|
|
2445
|
-
if (!Array.isArray(
|
|
2802
|
+
if (!Array.isArray(sent) || sent.length > 1000)
|
|
2446
2803
|
throw new HostError("invalid", "a layer holds at most 1000 marks");
|
|
2804
|
+
const marks = sent.map((mark) => ({ ...mark }));
|
|
2447
2805
|
for (const mark of marks) {
|
|
2448
2806
|
const time = mark.time;
|
|
2449
2807
|
if (typeof time !== "number" ||
|
|
@@ -2453,6 +2811,9 @@ export class TestHost {
|
|
|
2453
2811
|
throw new HostError("invalid", `time ${time} is outside the media`);
|
|
2454
2812
|
if (!["chapter", "bookmark", "marker"].includes(String(mark.kind)))
|
|
2455
2813
|
throw new HostError("invalid", "kind is chapter, bookmark or marker");
|
|
2814
|
+
// A text value is shown translated (`plugin.i18n.text`).
|
|
2815
|
+
if (isTextValue(mark.title))
|
|
2816
|
+
mark.title = this.#declared(mark.title, "title");
|
|
2456
2817
|
if (mark.title !== undefined &&
|
|
2457
2818
|
mark.title !== null &&
|
|
2458
2819
|
(typeof mark.title !== "string" || [...mark.title].length > 100))
|
|
@@ -2468,6 +2829,16 @@ export class TestHost {
|
|
|
2468
2829
|
"ui.interaction": (p) => void (this.mounted().locks = p),
|
|
2469
2830
|
"dialogs.confirm": (p) => {
|
|
2470
2831
|
this.confirms.push(p);
|
|
2832
|
+
const shown = (field, formatted = false) => p[field] === undefined
|
|
2833
|
+
? undefined
|
|
2834
|
+
: this.#shown(p[field], formatted && typeof p[field] === "string" ? p.args : undefined, field);
|
|
2835
|
+
this.confirmTexts.push({
|
|
2836
|
+
message: shown("message", true),
|
|
2837
|
+
...(p.detail === undefined ? {} : { detail: shown("detail", true) }),
|
|
2838
|
+
confirm: shown("confirm"),
|
|
2839
|
+
cancel: shown("cancel") ?? this.#hostText("Cancel"),
|
|
2840
|
+
...(p.checkbox === undefined ? {} : { checkbox: shown("checkbox") }),
|
|
2841
|
+
});
|
|
2471
2842
|
const answer = this.dialogAnswers.shift();
|
|
2472
2843
|
if (answer === undefined)
|
|
2473
2844
|
throw new Error("test host: no dialog answer queued");
|
|
@@ -2478,7 +2849,7 @@ export class TestHost {
|
|
|
2478
2849
|
"window.state": () => this.#windowFacts(),
|
|
2479
2850
|
"window.setVideoArea": (p) => {
|
|
2480
2851
|
if (this.mainWindow.fullscreen)
|
|
2481
|
-
throw new HostError("unavailable", "
|
|
2852
|
+
throw new HostError("unavailable", this.#hostText("Not available in fullscreen"));
|
|
2482
2853
|
const width = Math.max(318, Number(p.width));
|
|
2483
2854
|
const height = Math.max(120, Number(p.height));
|
|
2484
2855
|
const clamped = width !== p.width || height !== p.height;
|
|
@@ -2498,10 +2869,10 @@ export class TestHost {
|
|
|
2498
2869
|
if (p.on === pip.active)
|
|
2499
2870
|
return;
|
|
2500
2871
|
if (p.on && !pip.supported)
|
|
2501
|
-
throw new HostError("unsupported", "
|
|
2872
|
+
throw new HostError("unsupported", this.#hostText("Picture-in-picture is not supported on this system"));
|
|
2502
2873
|
const video = this.source.phase !== "empty" && this.source.hasVideo !== false;
|
|
2503
2874
|
if (p.on && !video)
|
|
2504
|
-
throw new HostError("unavailable", "
|
|
2875
|
+
throw new HostError("unavailable", this.#hostText("No video"));
|
|
2505
2876
|
this.setMainWindow({
|
|
2506
2877
|
// The player leaves fullscreen before the window opens.
|
|
2507
2878
|
fullscreen: p.on ? false : this.mainWindow.fullscreen,
|
|
@@ -2531,7 +2902,20 @@ export class TestHost {
|
|
|
2531
2902
|
loop: loop && { a: loop.a, b: loop.b, count: loop.count ?? null },
|
|
2532
2903
|
});
|
|
2533
2904
|
},
|
|
2534
|
-
"dialogs.open": () => {
|
|
2905
|
+
"dialogs.open": (p) => {
|
|
2906
|
+
const title = p.title === undefined ? undefined : this.#declared(p.title, "title");
|
|
2907
|
+
// A string file name is data; a text is shown translated, a path
|
|
2908
|
+
// separator in the translation replaced.
|
|
2909
|
+
const fileName = p.fileName === undefined
|
|
2910
|
+
? undefined
|
|
2911
|
+
: typeof p.fileName === "string"
|
|
2912
|
+
? p.fileName
|
|
2913
|
+
: this.#declared(p.fileName, "fileName").replace(/[/\\]/g, "-");
|
|
2914
|
+
this.fileDialogs.push({
|
|
2915
|
+
kind: String(p.kind),
|
|
2916
|
+
...(title === undefined ? {} : { title }),
|
|
2917
|
+
...(fileName === undefined ? {} : { fileName }),
|
|
2918
|
+
});
|
|
2535
2919
|
const answer = this.dialogAnswers.shift();
|
|
2536
2920
|
if (answer === undefined)
|
|
2537
2921
|
throw new Error("test host: no dialog answer queued");
|
|
@@ -2860,7 +3244,11 @@ export class TestHost {
|
|
|
2860
3244
|
: p.select === "secondary"
|
|
2861
3245
|
? "secondary"
|
|
2862
3246
|
: "primary";
|
|
2863
|
-
const title =
|
|
3247
|
+
const title = isTextValue(p.title)
|
|
3248
|
+
? this.#declared(p.title, "title")
|
|
3249
|
+
: typeof p.title === "string"
|
|
3250
|
+
? p.title
|
|
3251
|
+
: null;
|
|
2864
3252
|
if (title !== null && ([...title].length < 1 || [...title].length > 200))
|
|
2865
3253
|
throw new HostError("invalid", "subtitle title needs 1..200 characters");
|
|
2866
3254
|
const bytes = Buffer.concat(upload.bytes);
|
|
@@ -2877,7 +3265,8 @@ export class TestHost {
|
|
|
2877
3265
|
};
|
|
2878
3266
|
this.subtitles.tracks.push({
|
|
2879
3267
|
target,
|
|
2880
|
-
title: title ??
|
|
3268
|
+
title: title ??
|
|
3269
|
+
this.#hostText(format === "ass" ? "External ASS" : "External SRT"),
|
|
2881
3270
|
language: null,
|
|
2882
3271
|
styleSupported: true,
|
|
2883
3272
|
styleReason: null,
|
|
@@ -3157,6 +3546,8 @@ export class TestHost {
|
|
|
3157
3546
|
*/
|
|
3158
3547
|
export async function startPlugin(activate, options = {}) {
|
|
3159
3548
|
const host = options.host ?? new TestHost();
|
|
3549
|
+
if (options.locale !== undefined)
|
|
3550
|
+
host.locale = options.locale;
|
|
3160
3551
|
const directory = options.directory instanceof URL
|
|
3161
3552
|
? fileURLToPath(options.directory)
|
|
3162
3553
|
: (options.directory ?? process.cwd());
|
|
@@ -3169,6 +3560,7 @@ export async function startPlugin(activate, options = {}) {
|
|
|
3169
3560
|
manifest = { id: read.id, capabilities: read.capabilities };
|
|
3170
3561
|
}
|
|
3171
3562
|
host.manifestId = manifest.id;
|
|
3563
|
+
host.loadCatalog(directory);
|
|
3172
3564
|
const data = options.dataDirectory ?? host.dataDirectory;
|
|
3173
3565
|
mkdirSync(data, { recursive: true });
|
|
3174
3566
|
const instance = await createPlugin(host, manifest, directory, data, host.app);
|