@vestra-player/sdk 1.5.9 → 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 +26 -0
- package/dist/commands.d.ts +7 -4
- package/dist/i18n-tools.js +59 -3
- 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 +54 -5
- package/dist/plugin/testing.js +199 -49
- 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 +295 -47
- package/llms.txt +41 -41
- package/package.json +1 -1
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);
|
|
@@ -1789,9 +1889,12 @@ export class TestHost {
|
|
|
1789
1889
|
return {
|
|
1790
1890
|
...this.#split(key),
|
|
1791
1891
|
// What the user sees, like the player's binding table.
|
|
1792
|
-
title: shown?.display.title ?? declaration.title,
|
|
1892
|
+
title: shown?.display.title ?? String(declaration.title),
|
|
1793
1893
|
path: declaration.path?.length
|
|
1794
|
-
? (shown?.display.path ?? declaration.path)
|
|
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)))
|
|
1795
1898
|
: null,
|
|
1796
1899
|
scope: scope,
|
|
1797
1900
|
defaults: declaration.bindings,
|
|
@@ -2127,8 +2230,12 @@ export class TestHost {
|
|
|
2127
2230
|
});
|
|
2128
2231
|
throw new HostError("resolution", message);
|
|
2129
2232
|
};
|
|
2130
|
-
|
|
2131
|
-
|
|
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");
|
|
2132
2239
|
const result = (p.result ?? {});
|
|
2133
2240
|
const producer = this.producers.get(Number(result.producerId));
|
|
2134
2241
|
if (result.kind !== "stream")
|
|
@@ -2232,10 +2339,15 @@ export class TestHost {
|
|
|
2232
2339
|
throw new HostError("limit", "command registry limit reached");
|
|
2233
2340
|
const host = this;
|
|
2234
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);
|
|
2235
2347
|
this.commands.set(id, {
|
|
2236
2348
|
declaration,
|
|
2237
2349
|
get display() {
|
|
2238
|
-
return host.#display(declaration.path ?? [], declaration.title);
|
|
2350
|
+
return host.#display(declaration.path ?? [], declaration.title, this.state.reason);
|
|
2239
2351
|
},
|
|
2240
2352
|
state: p.state,
|
|
2241
2353
|
revision: Number(p.revision),
|
|
@@ -2256,6 +2368,7 @@ export class TestHost {
|
|
|
2256
2368
|
const command = this.command(String(p.id));
|
|
2257
2369
|
if (Number(p.revision) <= command.revision)
|
|
2258
2370
|
throw new HostError("command", `stale command revision ${p.revision}`);
|
|
2371
|
+
this.#checkReason(p.state);
|
|
2259
2372
|
command.revision = Number(p.revision);
|
|
2260
2373
|
command.state = p.state;
|
|
2261
2374
|
return { revision: command.revision };
|
|
@@ -2264,12 +2377,14 @@ export class TestHost {
|
|
|
2264
2377
|
const id = String(p.id);
|
|
2265
2378
|
if (this.windows.has(id))
|
|
2266
2379
|
throw new HostError("conflict", "view already exists");
|
|
2267
|
-
this.#
|
|
2380
|
+
const properties = this.#shownValues(p.properties);
|
|
2381
|
+
this.#checkValues(properties);
|
|
2382
|
+
this.#seeImages(properties);
|
|
2268
2383
|
const host = this;
|
|
2269
2384
|
this.windows.set(id, {
|
|
2270
2385
|
file: String(p.file),
|
|
2271
2386
|
visible: true,
|
|
2272
|
-
props: { ...
|
|
2387
|
+
props: { ...properties },
|
|
2273
2388
|
callbacks: p.callbacks,
|
|
2274
2389
|
userClose() {
|
|
2275
2390
|
host.windows.delete(id);
|
|
@@ -2287,8 +2402,10 @@ export class TestHost {
|
|
|
2287
2402
|
const window = this.windows.get(String(p.id));
|
|
2288
2403
|
if (!window)
|
|
2289
2404
|
throw new HostError("missing", "view not found");
|
|
2290
|
-
this.#
|
|
2291
|
-
|
|
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;
|
|
2292
2409
|
},
|
|
2293
2410
|
"ui.show": (p) => {
|
|
2294
2411
|
const window = this.windows.get(String(p.id));
|
|
@@ -2320,14 +2437,15 @@ export class TestHost {
|
|
|
2320
2437
|
throw taken(this.#hostText("The sidebar holds at most {} tabs", [
|
|
2321
2438
|
MAX_SIDEBAR_TABS,
|
|
2322
2439
|
]));
|
|
2323
|
-
this.#
|
|
2440
|
+
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
2441
|
+
this.#seeImages(properties);
|
|
2324
2442
|
const host = this;
|
|
2325
2443
|
this.panel = {
|
|
2326
2444
|
slot: String(p.slot),
|
|
2327
2445
|
file: String(p.file),
|
|
2328
2446
|
component: String(p.component),
|
|
2329
2447
|
visible: true,
|
|
2330
|
-
props: { ...
|
|
2448
|
+
props: { ...properties },
|
|
2331
2449
|
locks: null,
|
|
2332
2450
|
emit(callback, ...args) {
|
|
2333
2451
|
host.emit("ui.callback", {
|
|
@@ -2400,15 +2518,21 @@ export class TestHost {
|
|
|
2400
2518
|
state.detached = p.detached;
|
|
2401
2519
|
},
|
|
2402
2520
|
"osd.show": (p) => {
|
|
2403
|
-
const text = this
|
|
2521
|
+
const text = this.#shown(p.text, p.args, "text").replace(/[\n\r]/g, " ");
|
|
2404
2522
|
if (!text.trim() || [...text].length > 200)
|
|
2405
2523
|
throw new HostError("invalid", "osd text needs 1..200 characters");
|
|
2406
2524
|
this.osd.push(text);
|
|
2407
2525
|
},
|
|
2408
|
-
"osd.panel": (p) =>
|
|
2409
|
-
p.text === null
|
|
2410
|
-
|
|
2411
|
-
|
|
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
|
+
},
|
|
2412
2536
|
"playback.stats": () => this.stats,
|
|
2413
2537
|
"audio.addTrack": (p) => {
|
|
2414
2538
|
this.#audioCondition(p.expected);
|
|
@@ -2541,8 +2665,11 @@ export class TestHost {
|
|
|
2541
2665
|
this.emit("sequence.step", { direction });
|
|
2542
2666
|
},
|
|
2543
2667
|
"ui.setMount": (p) => {
|
|
2544
|
-
this
|
|
2545
|
-
|
|
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;
|
|
2546
2673
|
},
|
|
2547
2674
|
"timeline.marks": () => ({ marks: this.timeline.marks }),
|
|
2548
2675
|
"drops.provide": (p) => {
|
|
@@ -2579,7 +2706,8 @@ export class TestHost {
|
|
|
2579
2706
|
throw new HostError("invalid", `page ${id} already exists`);
|
|
2580
2707
|
if (this.settings.pages.size >= 8)
|
|
2581
2708
|
throw new HostError("invalid", "a plugin has at most 8 settings pages");
|
|
2582
|
-
this.#
|
|
2709
|
+
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
2710
|
+
this.#seeImages(properties);
|
|
2583
2711
|
const host = this;
|
|
2584
2712
|
this.settings.pages.set(id, {
|
|
2585
2713
|
category: String(p.category),
|
|
@@ -2588,7 +2716,7 @@ export class TestHost {
|
|
|
2588
2716
|
file: String(p.file),
|
|
2589
2717
|
component: String(p.component),
|
|
2590
2718
|
global: String(p.global),
|
|
2591
|
-
props: { ...
|
|
2719
|
+
props: { ...properties },
|
|
2592
2720
|
emit(callback, ...args) {
|
|
2593
2721
|
host.emit("ui.callback", {
|
|
2594
2722
|
view: `$settings:${id}`,
|
|
@@ -2602,8 +2730,10 @@ export class TestHost {
|
|
|
2602
2730
|
const page = this.settings.pages.get(String(p.id));
|
|
2603
2731
|
if (!page)
|
|
2604
2732
|
throw new HostError("missing", "page not registered");
|
|
2605
|
-
this.#
|
|
2606
|
-
|
|
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;
|
|
2607
2737
|
},
|
|
2608
2738
|
"settings.unregister": (p) => {
|
|
2609
2739
|
this.settings.pages.delete(String(p.id));
|
|
@@ -2667,10 +2797,11 @@ export class TestHost {
|
|
|
2667
2797
|
if (p.sourceId !== undefined &&
|
|
2668
2798
|
p.sourceId !== this.source.stamp.generation)
|
|
2669
2799
|
return { applied: false };
|
|
2670
|
-
const
|
|
2800
|
+
const sent = p.marks;
|
|
2671
2801
|
const duration = this.source.duration;
|
|
2672
|
-
if (!Array.isArray(
|
|
2802
|
+
if (!Array.isArray(sent) || sent.length > 1000)
|
|
2673
2803
|
throw new HostError("invalid", "a layer holds at most 1000 marks");
|
|
2804
|
+
const marks = sent.map((mark) => ({ ...mark }));
|
|
2674
2805
|
for (const mark of marks) {
|
|
2675
2806
|
const time = mark.time;
|
|
2676
2807
|
if (typeof time !== "number" ||
|
|
@@ -2680,6 +2811,9 @@ export class TestHost {
|
|
|
2680
2811
|
throw new HostError("invalid", `time ${time} is outside the media`);
|
|
2681
2812
|
if (!["chapter", "bookmark", "marker"].includes(String(mark.kind)))
|
|
2682
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");
|
|
2683
2817
|
if (mark.title !== undefined &&
|
|
2684
2818
|
mark.title !== null &&
|
|
2685
2819
|
(typeof mark.title !== "string" || [...mark.title].length > 100))
|
|
@@ -2695,10 +2829,9 @@ export class TestHost {
|
|
|
2695
2829
|
"ui.interaction": (p) => void (this.mounted().locks = p),
|
|
2696
2830
|
"dialogs.confirm": (p) => {
|
|
2697
2831
|
this.confirms.push(p);
|
|
2698
|
-
const
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
: undefined;
|
|
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);
|
|
2702
2835
|
this.confirmTexts.push({
|
|
2703
2836
|
message: shown("message", true),
|
|
2704
2837
|
...(p.detail === undefined ? {} : { detail: shown("detail", true) }),
|
|
@@ -2769,7 +2902,20 @@ export class TestHost {
|
|
|
2769
2902
|
loop: loop && { a: loop.a, b: loop.b, count: loop.count ?? null },
|
|
2770
2903
|
});
|
|
2771
2904
|
},
|
|
2772
|
-
"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
|
+
});
|
|
2773
2919
|
const answer = this.dialogAnswers.shift();
|
|
2774
2920
|
if (answer === undefined)
|
|
2775
2921
|
throw new Error("test host: no dialog answer queued");
|
|
@@ -3098,7 +3244,11 @@ export class TestHost {
|
|
|
3098
3244
|
: p.select === "secondary"
|
|
3099
3245
|
? "secondary"
|
|
3100
3246
|
: "primary";
|
|
3101
|
-
const title =
|
|
3247
|
+
const title = isTextValue(p.title)
|
|
3248
|
+
? this.#declared(p.title, "title")
|
|
3249
|
+
: typeof p.title === "string"
|
|
3250
|
+
? p.title
|
|
3251
|
+
: null;
|
|
3102
3252
|
if (title !== null && ([...title].length < 1 || [...title].length > 200))
|
|
3103
3253
|
throw new HostError("invalid", "subtitle title needs 1..200 characters");
|
|
3104
3254
|
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)
|