@vestra-player/sdk 1.5.7 → 1.5.9
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 +22 -0
- package/README.md +4 -1
- package/bin/vestra-plugin.mjs +194 -17
- package/dist/commands.d.ts +1 -2
- package/dist/commands.js +1 -2
- package/dist/i18n-tools.d.ts +67 -0
- package/dist/i18n-tools.js +731 -0
- package/dist/i18n.d.ts +130 -0
- package/dist/i18n.js +673 -0
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/appearance.js +27 -1
- package/dist/plugin/language.d.ts +4 -0
- package/dist/plugin/language.js +5 -0
- package/dist/plugin/osd.d.ts +3 -0
- package/dist/plugin/osd.js +45 -9
- package/dist/plugin/packages.js +11 -65
- package/dist/plugin/resolvers.js +64 -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/testing.d.ts +155 -4
- package/dist/plugin/testing.js +332 -50
- package/dist/plugin/types.d.ts +247 -46
- package/dist/plugin/ui.js +4 -1
- package/dist/plugin/window.d.ts +4 -0
- package/dist/plugin/window.js +8 -0
- package/dist/wire.d.ts +29 -14
- package/dist/wire.js +12 -12
- package/llms-full.txt +399 -52
- 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
|
@@ -69,10 +69,27 @@ import { nodeProblem } from "../processing.js";
|
|
|
69
69
|
import { RENDER_DEFAULTS, renderProblem, VIEW_DEFAULTS, viewProblem, } from "./view.js";
|
|
70
70
|
import { createPlugin } from "./index.js";
|
|
71
71
|
import { THEMES } from "./appearance.js";
|
|
72
|
+
import { Catalog, compileCatalog, format as formatTemplate, HOST_GROUPS, HOST_ZH_HANS, LOCALES, menuKeys, parsePo, } from "../i18n.js";
|
|
72
73
|
/** Throw one of these from `host.handle(...)` to simulate a definite refusal. */
|
|
73
74
|
export { HostError };
|
|
74
75
|
/** The player's command caps, which the test host enforces as well. */
|
|
75
76
|
export { COMMAND_LIMITS, MENU_IDS };
|
|
77
|
+
/**
|
|
78
|
+
* The player's interface languages, English (the source) first, as the
|
|
79
|
+
* player's own table: `["en", "zh-Hans", "ja", "ko", "de", "fr", "es"]`.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* import { LOCALES } from "@vestra-player/sdk/testing";
|
|
84
|
+
*
|
|
85
|
+
* for (const locale of LOCALES.slice(1)) {
|
|
86
|
+
* const { host, stop } = await startPlugin(activate, { directory, locale });
|
|
87
|
+
* assert.notEqual(host.command("open").display.title, "Open…");
|
|
88
|
+
* await stop();
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export { LOCALES };
|
|
76
93
|
const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
77
94
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
78
95
|
function initialSource() {
|
|
@@ -121,9 +138,15 @@ const taken = (message) => {
|
|
|
121
138
|
return error;
|
|
122
139
|
};
|
|
123
140
|
const SLOT_LABELS = {
|
|
124
|
-
bottom: "
|
|
125
|
-
sidebar: "
|
|
126
|
-
backdrop: "
|
|
141
|
+
bottom: "bottom bar",
|
|
142
|
+
sidebar: "sidebar",
|
|
143
|
+
backdrop: "video backdrop",
|
|
144
|
+
};
|
|
145
|
+
/** Texts only the test host uses, with their Chinese of SDK 1.5.8. */
|
|
146
|
+
const TEST_ZH_HANS = {
|
|
147
|
+
"this site": "这个网站",
|
|
148
|
+
"External SRT": "外部 SRT",
|
|
149
|
+
"External ASS": "外部 ASS",
|
|
127
150
|
};
|
|
128
151
|
/** The player's limit of sidebar tabs. */
|
|
129
152
|
const MAX_SIDEBAR_TABS = 8;
|
|
@@ -227,10 +250,133 @@ export class TestHost {
|
|
|
227
250
|
bottom: null,
|
|
228
251
|
sidebar: null,
|
|
229
252
|
};
|
|
230
|
-
/**
|
|
253
|
+
/**
|
|
254
|
+
* Every `osd.show` text as the player shows it (translated with the
|
|
255
|
+
* plugin's catalog and formatted with its args), newest last. The
|
|
256
|
+
* template and args the plugin sent are in `host.sent("osd.show")`.
|
|
257
|
+
*/
|
|
231
258
|
osd = [];
|
|
232
|
-
/** The text of this plugin's osd panel, `null` when hidden. */
|
|
259
|
+
/** The text of this plugin's osd panel as shown, `null` when hidden. */
|
|
233
260
|
osdPanel = null;
|
|
261
|
+
#locale;
|
|
262
|
+
/**
|
|
263
|
+
* The interface language the test host plays (`plugin.i18n.locale`), `"en"`
|
|
264
|
+
* by default. Set it before {@link startPlugin} (or pass its `locale`
|
|
265
|
+
* option): the plugin's catalog for it is read from the package's
|
|
266
|
+
* `locales/` then, and the menus ({@link FakeCommand.display}), OSD
|
|
267
|
+
* messages, dialog texts, settings page and panel titles show its
|
|
268
|
+
* translations, the way the player shows them.
|
|
269
|
+
*
|
|
270
|
+
* The player's own texts (core actions' messages and menus, its
|
|
271
|
+
* refusals) are English, or the player's Simplified Chinese for
|
|
272
|
+
* `"zh-Hans"`. While `locale` was never set they stay in Chinese, as
|
|
273
|
+
* players up to 0.4.2 showed them, so tests written for SDK 1.5.8 keep
|
|
274
|
+
* passing; set `locale` to test what a user sees now.
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* ```ts
|
|
278
|
+
* const { host, stop } = await startPlugin(activate, { directory, locale: "zh-Hans" });
|
|
279
|
+
* assert.deepEqual(host.command("blur").display.path, ["视频", "模糊…"]);
|
|
280
|
+
* await stop();
|
|
281
|
+
* ```
|
|
282
|
+
*
|
|
283
|
+
* @since 1.5.9
|
|
284
|
+
*/
|
|
285
|
+
get locale() {
|
|
286
|
+
return this.#locale ?? "en";
|
|
287
|
+
}
|
|
288
|
+
set locale(locale) {
|
|
289
|
+
if (!LOCALES.includes(locale))
|
|
290
|
+
throw new Error(`test host: unknown locale ${locale}`);
|
|
291
|
+
this.#locale = locale;
|
|
292
|
+
}
|
|
293
|
+
/** The plugin's translations for `locale` (see `loadCatalog`). */
|
|
294
|
+
#catalog = new Catalog();
|
|
295
|
+
/**
|
|
296
|
+
* Read the plugin's translations for {@link TestHost.locale} from a
|
|
297
|
+
* package directory, as the player would (`locales/<locale>.json` of a
|
|
298
|
+
* built package, else the source `locales/<locale>.po`); `startPlugin`
|
|
299
|
+
* does this. None for English; a missing file means none; a broken `.po`
|
|
300
|
+
* throws.
|
|
301
|
+
*
|
|
302
|
+
* @param directory - The package directory (holding `locales/`).
|
|
303
|
+
* @since 1.5.9
|
|
304
|
+
*/
|
|
305
|
+
loadCatalog(directory) {
|
|
306
|
+
this.#catalog = new 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);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* A text of the plugin as the player shows it: translated with the
|
|
319
|
+
* plugin's catalog ({@link TestHost.loadCatalog}), then formatted with
|
|
320
|
+
* `args` when given.
|
|
321
|
+
*
|
|
322
|
+
* @param text - English source text or template.
|
|
323
|
+
* @param args - Values for its placeholders.
|
|
324
|
+
* @returns The shown text.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* assert.equal(host.translate("Speed {}×", [2]), "倍速 2×");
|
|
329
|
+
* ```
|
|
330
|
+
*
|
|
331
|
+
* @since 1.5.9
|
|
332
|
+
*/
|
|
333
|
+
translate(text, args) {
|
|
334
|
+
return this.#catalog.template(text, args ?? null);
|
|
335
|
+
}
|
|
336
|
+
/** A text of the player itself, in the player's language (see `locale`). */
|
|
337
|
+
#hostText(text, args) {
|
|
338
|
+
const zh = this.#locale === undefined || this.#locale === "zh-Hans";
|
|
339
|
+
const shown = (zh && (HOST_ZH_HANS[text] ?? TEST_ZH_HANS[text])) || text;
|
|
340
|
+
return args === undefined ? shown : formatTemplate(shown, args);
|
|
341
|
+
}
|
|
342
|
+
/** A declared menu path and title as the player shows them. */
|
|
343
|
+
#display(path, title) {
|
|
344
|
+
const keys = menuKeys(path);
|
|
345
|
+
return {
|
|
346
|
+
title: this.translate(title),
|
|
347
|
+
// The player's submenus by key, everything else as declared.
|
|
348
|
+
path: keys.map((segment, index) => index < keys.length - 1 &&
|
|
349
|
+
HOST_GROUPS.includes(keys.slice(0, index + 1).join("/"))
|
|
350
|
+
? this.#hostText(segment)
|
|
351
|
+
: this.translate(path[index] ?? segment)),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The saved interface language (`plugin.appearance.language`), changed by
|
|
356
|
+
* `appearance.setLanguage` or {@link TestHost.setLanguage}.
|
|
357
|
+
*
|
|
358
|
+
* @since 1.5.9
|
|
359
|
+
*/
|
|
360
|
+
language = "system";
|
|
361
|
+
/**
|
|
362
|
+
* Simulate another plugin (or the user) saving the interface language:
|
|
363
|
+
* pushes `appearance.changed`.
|
|
364
|
+
*
|
|
365
|
+
* @param language - `"system"` or a locale.
|
|
366
|
+
* @since 1.5.9
|
|
367
|
+
*/
|
|
368
|
+
setLanguage(language) {
|
|
369
|
+
this.language = language;
|
|
370
|
+
this.emit("appearance.changed", { theme: this.appearance, language });
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Every `dialogs.confirm` as the player shows it: its texts translated
|
|
374
|
+
* (message and detail formatted with `args`), newest last. The options
|
|
375
|
+
* the plugin sent are in {@link TestHost.confirms}.
|
|
376
|
+
*
|
|
377
|
+
* @since 1.5.9
|
|
378
|
+
*/
|
|
379
|
+
confirmTexts = [];
|
|
234
380
|
/** The sequence provider's flags, `null` without a provider. */
|
|
235
381
|
sequence = null;
|
|
236
382
|
/**
|
|
@@ -773,6 +919,43 @@ export class TestHost {
|
|
|
773
919
|
* ```
|
|
774
920
|
*/
|
|
775
921
|
otherResolvers = [];
|
|
922
|
+
/**
|
|
923
|
+
* What `plugin.resolvers.fileTypes` reports: the extensions of this
|
|
924
|
+
* plugin's file resolvers and of {@link TestHost.otherResolvers}, sorted,
|
|
925
|
+
* each once.
|
|
926
|
+
*/
|
|
927
|
+
fileTypes() {
|
|
928
|
+
const types = new Set();
|
|
929
|
+
for (const declaration of [
|
|
930
|
+
...this.resolvers.values(),
|
|
931
|
+
...this.otherResolvers,
|
|
932
|
+
])
|
|
933
|
+
for (const type of declaration.extensions ?? [])
|
|
934
|
+
types.add(type);
|
|
935
|
+
return [...types].sort();
|
|
936
|
+
}
|
|
937
|
+
#sentTypes = "[]";
|
|
938
|
+
/**
|
|
939
|
+
* Push `resolvers.fileTypes` when {@link TestHost.fileTypes} changed:
|
|
940
|
+
* called after this plugin's resolvers come and go; call it yourself
|
|
941
|
+
* after changing {@link TestHost.otherResolvers} (another plugin started
|
|
942
|
+
* or stopped).
|
|
943
|
+
*
|
|
944
|
+
* @example
|
|
945
|
+
* ```ts
|
|
946
|
+
* host.otherResolvers.push({ owner: "vestra.nes", extensions: ["nes"] });
|
|
947
|
+
* host.updateFileTypes();
|
|
948
|
+
* await host.settle();
|
|
949
|
+
* assert.deepEqual(plugin.resolvers.fileTypes, ["nes"]);
|
|
950
|
+
* ```
|
|
951
|
+
*/
|
|
952
|
+
updateFileTypes() {
|
|
953
|
+
const types = this.fileTypes();
|
|
954
|
+
if (JSON.stringify(types) === this.#sentTypes)
|
|
955
|
+
return;
|
|
956
|
+
this.#sentTypes = JSON.stringify(types);
|
|
957
|
+
this.emit("resolvers.fileTypes", { fileTypes: types });
|
|
958
|
+
}
|
|
776
959
|
/**
|
|
777
960
|
* File opens waiting for this plugin's file resolver (`extensions`), by
|
|
778
961
|
* request id: the path and how it was opened. Opening a matching path
|
|
@@ -976,14 +1159,14 @@ export class TestHost {
|
|
|
976
1159
|
/** Why one frame that way is impossible (B07), or null; the player's rules. */
|
|
977
1160
|
#frameCheck(forward) {
|
|
978
1161
|
const s = this.source;
|
|
979
|
-
const unsupported = "
|
|
980
|
-
const noBack = "
|
|
1162
|
+
const unsupported = this.#hostText("Not supported by the current source");
|
|
1163
|
+
const noBack = this.#hostText("The current source cannot step back a frame");
|
|
981
1164
|
if (s.phase !== "ready" && s.phase !== "ended")
|
|
982
1165
|
return unsupported;
|
|
983
1166
|
if (s.kind !== "media")
|
|
984
1167
|
return forward ? unsupported : noBack;
|
|
985
1168
|
if (s.hasVideo === false)
|
|
986
|
-
return "
|
|
1169
|
+
return this.#hostText("No video");
|
|
987
1170
|
if (forward && s.phase !== "ready")
|
|
988
1171
|
return unsupported;
|
|
989
1172
|
if (!forward && !s.capabilities.seek)
|
|
@@ -994,12 +1177,16 @@ export class TestHost {
|
|
|
994
1177
|
#coreState(action) {
|
|
995
1178
|
const f = this.#facts();
|
|
996
1179
|
const s = this.source;
|
|
997
|
-
const unsupported = "
|
|
1180
|
+
const unsupported = this.#hostText("Not supported by the current source");
|
|
998
1181
|
const rule = {
|
|
999
1182
|
"play-pause": [f.toggle, f.ready ? s.paused : null, unsupported],
|
|
1000
1183
|
stop: [f.close, null, unsupported],
|
|
1001
|
-
previous: [
|
|
1002
|
-
|
|
1184
|
+
previous: [
|
|
1185
|
+
!!this.sequence?.canPrevious,
|
|
1186
|
+
null,
|
|
1187
|
+
this.#hostText("No previous item"),
|
|
1188
|
+
],
|
|
1189
|
+
next: [!!this.sequence?.canNext, null, this.#hostText("No next item")],
|
|
1003
1190
|
"frame-back": [
|
|
1004
1191
|
!this.#frameCheck(false),
|
|
1005
1192
|
null,
|
|
@@ -1024,11 +1211,18 @@ export class TestHost {
|
|
|
1024
1211
|
}
|
|
1025
1212
|
#registerCoreActions() {
|
|
1026
1213
|
const table = [
|
|
1027
|
-
["stop", ["
|
|
1028
|
-
[
|
|
1214
|
+
["stop", ["Close"], 120, "open", ["F4"], false],
|
|
1215
|
+
[
|
|
1216
|
+
"play-pause",
|
|
1217
|
+
["Playback", "Play/Pause"],
|
|
1218
|
+
201,
|
|
1219
|
+
"domain",
|
|
1220
|
+
["Space"],
|
|
1221
|
+
false,
|
|
1222
|
+
],
|
|
1029
1223
|
[
|
|
1030
1224
|
"previous",
|
|
1031
|
-
["
|
|
1225
|
+
["Playback", "Previous"],
|
|
1032
1226
|
202,
|
|
1033
1227
|
"domain",
|
|
1034
1228
|
["PageUp", "Mouse:X1"],
|
|
@@ -1036,17 +1230,31 @@ export class TestHost {
|
|
|
1036
1230
|
],
|
|
1037
1231
|
[
|
|
1038
1232
|
"next",
|
|
1039
|
-
["
|
|
1233
|
+
["Playback", "Next"],
|
|
1040
1234
|
203,
|
|
1041
1235
|
"domain",
|
|
1042
1236
|
["PageDown", "Mouse:X2"],
|
|
1043
1237
|
false,
|
|
1044
1238
|
],
|
|
1045
|
-
[
|
|
1046
|
-
|
|
1239
|
+
[
|
|
1240
|
+
"frame-back",
|
|
1241
|
+
["Playback", "Seek", "Previous Frame"],
|
|
1242
|
+
204,
|
|
1243
|
+
"frame",
|
|
1244
|
+
["D"],
|
|
1245
|
+
true,
|
|
1246
|
+
],
|
|
1247
|
+
[
|
|
1248
|
+
"frame-forward",
|
|
1249
|
+
["Playback", "Seek", "Next Frame"],
|
|
1250
|
+
205,
|
|
1251
|
+
"frame",
|
|
1252
|
+
["F"],
|
|
1253
|
+
true,
|
|
1254
|
+
],
|
|
1047
1255
|
[
|
|
1048
1256
|
"seek-backward",
|
|
1049
|
-
["
|
|
1257
|
+
["Playback", "Seek", "Back 5 s"],
|
|
1050
1258
|
210,
|
|
1051
1259
|
"step",
|
|
1052
1260
|
["ArrowLeft"],
|
|
@@ -1054,7 +1262,7 @@ export class TestHost {
|
|
|
1054
1262
|
],
|
|
1055
1263
|
[
|
|
1056
1264
|
"seek-forward",
|
|
1057
|
-
["
|
|
1265
|
+
["Playback", "Seek", "Forward 5 s"],
|
|
1058
1266
|
211,
|
|
1059
1267
|
"step",
|
|
1060
1268
|
["ArrowRight"],
|
|
@@ -1062,18 +1270,32 @@ export class TestHost {
|
|
|
1062
1270
|
],
|
|
1063
1271
|
[
|
|
1064
1272
|
"rate-normal",
|
|
1065
|
-
["
|
|
1273
|
+
["Playback", "Speed", "Normal"],
|
|
1066
1274
|
220,
|
|
1067
1275
|
"domain",
|
|
1068
1276
|
["Z"],
|
|
1069
1277
|
false,
|
|
1070
1278
|
true,
|
|
1071
1279
|
],
|
|
1072
|
-
[
|
|
1073
|
-
|
|
1280
|
+
[
|
|
1281
|
+
"rate-down",
|
|
1282
|
+
["Playback", "Speed", "Slower 0.1"],
|
|
1283
|
+
221,
|
|
1284
|
+
"domain",
|
|
1285
|
+
["X"],
|
|
1286
|
+
false,
|
|
1287
|
+
],
|
|
1288
|
+
[
|
|
1289
|
+
"rate-up",
|
|
1290
|
+
["Playback", "Speed", "Faster 0.1"],
|
|
1291
|
+
222,
|
|
1292
|
+
"domain",
|
|
1293
|
+
["C"],
|
|
1294
|
+
false,
|
|
1295
|
+
],
|
|
1074
1296
|
[
|
|
1075
1297
|
"volume-up",
|
|
1076
|
-
["
|
|
1298
|
+
["Audio", "Volume +"],
|
|
1077
1299
|
501,
|
|
1078
1300
|
"domain",
|
|
1079
1301
|
["ArrowUp", "Mouse:WheelUp"],
|
|
@@ -1081,13 +1303,13 @@ export class TestHost {
|
|
|
1081
1303
|
],
|
|
1082
1304
|
[
|
|
1083
1305
|
"volume-down",
|
|
1084
|
-
["
|
|
1306
|
+
["Audio", "Volume −"],
|
|
1085
1307
|
502,
|
|
1086
1308
|
"domain",
|
|
1087
1309
|
["ArrowDown", "Mouse:WheelDown"],
|
|
1088
1310
|
true,
|
|
1089
1311
|
],
|
|
1090
|
-
["mute", ["
|
|
1312
|
+
["mute", ["Audio", "Mute"], 503, "domain", ["M"], false],
|
|
1091
1313
|
];
|
|
1092
1314
|
const host = this;
|
|
1093
1315
|
for (const [id, path, order, group, bindings, repeat, radio] of table) {
|
|
@@ -1101,6 +1323,18 @@ export class TestHost {
|
|
|
1101
1323
|
bindings,
|
|
1102
1324
|
repeat,
|
|
1103
1325
|
},
|
|
1326
|
+
// The player's own words in the player's language.
|
|
1327
|
+
get display() {
|
|
1328
|
+
const leaf = path.at(-1);
|
|
1329
|
+
const step = /^(Back|Forward) (\S+) s$/.exec(leaf);
|
|
1330
|
+
const title = step
|
|
1331
|
+
? host.#hostText(`${step[1]} {} s`, [step[2]])
|
|
1332
|
+
: host.#hostText(leaf);
|
|
1333
|
+
return {
|
|
1334
|
+
title,
|
|
1335
|
+
path: [...path.slice(0, -1).map((s) => host.#hostText(s)), title],
|
|
1336
|
+
};
|
|
1337
|
+
},
|
|
1104
1338
|
get state() {
|
|
1105
1339
|
return host.#coreState(id);
|
|
1106
1340
|
},
|
|
@@ -1138,7 +1372,7 @@ export class TestHost {
|
|
|
1138
1372
|
kind: "control",
|
|
1139
1373
|
control: { kind: "pause", paused: !s.paused },
|
|
1140
1374
|
});
|
|
1141
|
-
this.osd.push(s.phase !== "ended" && !s.paused ? "
|
|
1375
|
+
this.osd.push(this.#hostText(s.phase !== "ended" && !s.paused ? "Paused" : "Playing"));
|
|
1142
1376
|
return;
|
|
1143
1377
|
case "stop":
|
|
1144
1378
|
this.#intent({ kind: "close" });
|
|
@@ -1163,7 +1397,7 @@ export class TestHost {
|
|
|
1163
1397
|
kind: "control",
|
|
1164
1398
|
control: { kind: "seek", seconds: target },
|
|
1165
1399
|
});
|
|
1166
|
-
this.osd.push(
|
|
1400
|
+
this.osd.push(this.#hostText(delta < 0 ? "Back {} s: {} / {}" : "Forward {} s: {} / {}", [Math.abs(delta), time(target), time(s.duration)]));
|
|
1167
1401
|
return;
|
|
1168
1402
|
}
|
|
1169
1403
|
case "rate-normal":
|
|
@@ -1173,7 +1407,7 @@ export class TestHost {
|
|
|
1173
1407
|
? 1
|
|
1174
1408
|
: Math.max(0.25, Math.min(4, Math.round((s.rate + (action === "rate-up" ? 0.1 : -0.1)) * 100) / 100));
|
|
1175
1409
|
this.#intent({ kind: "control", control: { kind: "rate", rate } });
|
|
1176
|
-
this.osd.push(
|
|
1410
|
+
this.osd.push(this.#hostText("Speed: {}x", [rateText(rate)]));
|
|
1177
1411
|
return;
|
|
1178
1412
|
}
|
|
1179
1413
|
case "volume-up":
|
|
@@ -1184,12 +1418,12 @@ export class TestHost {
|
|
|
1184
1418
|
? Math.max(s.output.volume, Math.min(100, Math.round(s.output.volume + 5)))
|
|
1185
1419
|
: Math.max(0, Math.min(200, Math.round(s.output.volume - 5)));
|
|
1186
1420
|
this.setSource({ output: { volume, muted: false } });
|
|
1187
|
-
this.osd.push(
|
|
1421
|
+
this.osd.push(this.#hostText("Volume: {}%", [volume]));
|
|
1188
1422
|
return;
|
|
1189
1423
|
}
|
|
1190
1424
|
case "mute":
|
|
1191
1425
|
this.setSource({ output: { ...s.output, muted: !s.output.muted } });
|
|
1192
|
-
this.osd.push(s.output.muted ? "
|
|
1426
|
+
this.osd.push(this.#hostText(s.output.muted ? "Mute: Off" : "Mute: On"));
|
|
1193
1427
|
return;
|
|
1194
1428
|
}
|
|
1195
1429
|
}
|
|
@@ -1361,6 +1595,9 @@ export class TestHost {
|
|
|
1361
1595
|
videoArea: { ...this.mainWindow.videoArea },
|
|
1362
1596
|
pictureInPicture: { ...this.mainWindow.pictureInPicture },
|
|
1363
1597
|
appearance: this.appearance,
|
|
1598
|
+
fileTypes: this.fileTypes(),
|
|
1599
|
+
language: this.language,
|
|
1600
|
+
locale: this.locale,
|
|
1364
1601
|
};
|
|
1365
1602
|
}
|
|
1366
1603
|
/**
|
|
@@ -1548,10 +1785,14 @@ export class TestHost {
|
|
|
1548
1785
|
const key = this.#bindingKey(id);
|
|
1549
1786
|
const scope = declaration.focus ? "panel" : "global";
|
|
1550
1787
|
const current = this.bindings.overlay.get(key) ?? declaration.bindings;
|
|
1788
|
+
const shown = this.commands.get(id) ?? this.coreCommands.get(id);
|
|
1551
1789
|
return {
|
|
1552
1790
|
...this.#split(key),
|
|
1553
|
-
|
|
1554
|
-
|
|
1791
|
+
// What the user sees, like the player's binding table.
|
|
1792
|
+
title: shown?.display.title ?? declaration.title,
|
|
1793
|
+
path: declaration.path?.length
|
|
1794
|
+
? (shown?.display.path ?? declaration.path)
|
|
1795
|
+
: null,
|
|
1555
1796
|
scope: scope,
|
|
1556
1797
|
defaults: declaration.bindings,
|
|
1557
1798
|
current,
|
|
@@ -1860,8 +2101,10 @@ export class TestHost {
|
|
|
1860
2101
|
for (const other of this.otherResolvers) {
|
|
1861
2102
|
const what = claims(other);
|
|
1862
2103
|
if (what) {
|
|
1863
|
-
const target = what === "host"
|
|
1864
|
-
|
|
2104
|
+
const target = what === "host"
|
|
2105
|
+
? this.#hostText("this site")
|
|
2106
|
+
: this.#hostText("File type .{}", [what]);
|
|
2107
|
+
throw taken(this.#hostText("{} is resolved by {}", [target, other.owner]));
|
|
1865
2108
|
}
|
|
1866
2109
|
}
|
|
1867
2110
|
this.resolvers.set(id, p);
|
|
@@ -1988,8 +2231,12 @@ export class TestHost {
|
|
|
1988
2231
|
this.commands.size + this.coreCommands.size >= COMMAND_LIMITS.total)
|
|
1989
2232
|
throw new HostError("limit", "command registry limit reached");
|
|
1990
2233
|
const host = this;
|
|
2234
|
+
const declaration = p.declaration;
|
|
1991
2235
|
this.commands.set(id, {
|
|
1992
|
-
declaration
|
|
2236
|
+
declaration,
|
|
2237
|
+
get display() {
|
|
2238
|
+
return host.#display(declaration.path ?? [], declaration.title);
|
|
2239
|
+
},
|
|
1993
2240
|
state: p.state,
|
|
1994
2241
|
revision: Number(p.revision),
|
|
1995
2242
|
async run() {
|
|
@@ -2064,10 +2311,15 @@ export class TestHost {
|
|
|
2064
2311
|
? this.layout.bottom?.panels?.[0]?.plugin
|
|
2065
2312
|
: undefined;
|
|
2066
2313
|
if (holder !== undefined || (p.slot === "bottom" && this.layout.bottom))
|
|
2067
|
-
throw taken(
|
|
2314
|
+
throw taken(this.#hostText("The {} is used by {}", [
|
|
2315
|
+
this.#hostText(SLOT_LABELS[String(p.slot)]),
|
|
2316
|
+
String(holder),
|
|
2317
|
+
]));
|
|
2068
2318
|
if (p.slot === "sidebar" &&
|
|
2069
2319
|
(this.layout.sidebar?.panels?.length ?? 0) >= MAX_SIDEBAR_TABS)
|
|
2070
|
-
throw taken(
|
|
2320
|
+
throw taken(this.#hostText("The sidebar holds at most {} tabs", [
|
|
2321
|
+
MAX_SIDEBAR_TABS,
|
|
2322
|
+
]));
|
|
2071
2323
|
this.#seeImages(p.properties);
|
|
2072
2324
|
const host = this;
|
|
2073
2325
|
this.panel = {
|
|
@@ -2093,7 +2345,9 @@ export class TestHost {
|
|
|
2093
2345
|
this.#addPanel(slot, {
|
|
2094
2346
|
owner: 1,
|
|
2095
2347
|
plugin: this.manifestId,
|
|
2096
|
-
title: typeof p.title === "string" && p.title
|
|
2348
|
+
title: typeof p.title === "string" && p.title
|
|
2349
|
+
? this.translate(p.title)
|
|
2350
|
+
: this.manifestId,
|
|
2097
2351
|
});
|
|
2098
2352
|
return { status: "ready", prepareMs: 0, shellGeneration: 1 };
|
|
2099
2353
|
},
|
|
@@ -2146,12 +2400,15 @@ export class TestHost {
|
|
|
2146
2400
|
state.detached = p.detached;
|
|
2147
2401
|
},
|
|
2148
2402
|
"osd.show": (p) => {
|
|
2149
|
-
const text = String(p.text);
|
|
2150
|
-
if (!text || [...text].length > 200)
|
|
2403
|
+
const text = this.translate(String(p.text), p.args).replace(/[\n\r]/g, " ");
|
|
2404
|
+
if (!text.trim() || [...text].length > 200)
|
|
2151
2405
|
throw new HostError("invalid", "osd text needs 1..200 characters");
|
|
2152
2406
|
this.osd.push(text);
|
|
2153
2407
|
},
|
|
2154
|
-
"osd.panel": (p) => void (this.osdPanel =
|
|
2408
|
+
"osd.panel": (p) => void (this.osdPanel =
|
|
2409
|
+
p.text === null
|
|
2410
|
+
? null
|
|
2411
|
+
: this.translate(String(p.text), p.args)),
|
|
2155
2412
|
"playback.stats": () => this.stats,
|
|
2156
2413
|
"audio.addTrack": (p) => {
|
|
2157
2414
|
this.#audioCondition(p.expected);
|
|
@@ -2254,7 +2511,7 @@ export class TestHost {
|
|
|
2254
2511
|
const reason = p.unit === "keyframe"
|
|
2255
2512
|
? s.phase === "ready" && s.capabilities.seek && s.hasVideo !== false
|
|
2256
2513
|
? null
|
|
2257
|
-
: "
|
|
2514
|
+
: this.#hostText("Not supported by the current source")
|
|
2258
2515
|
: this.#frameCheck(p.direction === "forward");
|
|
2259
2516
|
if (reason)
|
|
2260
2517
|
throw new HostError("unsupported", reason);
|
|
@@ -2301,6 +2558,14 @@ export class TestHost {
|
|
|
2301
2558
|
this.dropProviders.splice(index, 1);
|
|
2302
2559
|
},
|
|
2303
2560
|
"drops.listen": (p) => void (this.panelDrops = p.listen === true),
|
|
2561
|
+
"appearance.setLanguage": (p) => {
|
|
2562
|
+
const language = String(p.language);
|
|
2563
|
+
if (language !== "system" &&
|
|
2564
|
+
!LOCALES.includes(language))
|
|
2565
|
+
throw new HostError("invalid", `unknown language ${language}`);
|
|
2566
|
+
if (language !== this.language)
|
|
2567
|
+
this.setLanguage(language);
|
|
2568
|
+
},
|
|
2304
2569
|
"appearance.set": (p) => {
|
|
2305
2570
|
// Like the player: an id it does not have selects the default (the
|
|
2306
2571
|
// SDK's own `set` refuses one before it gets here).
|
|
@@ -2318,7 +2583,7 @@ export class TestHost {
|
|
|
2318
2583
|
const host = this;
|
|
2319
2584
|
this.settings.pages.set(id, {
|
|
2320
2585
|
category: String(p.category),
|
|
2321
|
-
title: String(p.title),
|
|
2586
|
+
title: this.translate(String(p.title)),
|
|
2322
2587
|
order: Number(p.order ?? 0),
|
|
2323
2588
|
file: String(p.file),
|
|
2324
2589
|
component: String(p.component),
|
|
@@ -2430,6 +2695,17 @@ export class TestHost {
|
|
|
2430
2695
|
"ui.interaction": (p) => void (this.mounted().locks = p),
|
|
2431
2696
|
"dialogs.confirm": (p) => {
|
|
2432
2697
|
this.confirms.push(p);
|
|
2698
|
+
const args = p.args;
|
|
2699
|
+
const shown = (field, formatted = false) => typeof p[field] === "string"
|
|
2700
|
+
? this.translate(p[field], formatted ? args : undefined)
|
|
2701
|
+
: undefined;
|
|
2702
|
+
this.confirmTexts.push({
|
|
2703
|
+
message: shown("message", true),
|
|
2704
|
+
...(p.detail === undefined ? {} : { detail: shown("detail", true) }),
|
|
2705
|
+
confirm: shown("confirm"),
|
|
2706
|
+
cancel: shown("cancel") ?? this.#hostText("Cancel"),
|
|
2707
|
+
...(p.checkbox === undefined ? {} : { checkbox: shown("checkbox") }),
|
|
2708
|
+
});
|
|
2433
2709
|
const answer = this.dialogAnswers.shift();
|
|
2434
2710
|
if (answer === undefined)
|
|
2435
2711
|
throw new Error("test host: no dialog answer queued");
|
|
@@ -2440,7 +2716,7 @@ export class TestHost {
|
|
|
2440
2716
|
"window.state": () => this.#windowFacts(),
|
|
2441
2717
|
"window.setVideoArea": (p) => {
|
|
2442
2718
|
if (this.mainWindow.fullscreen)
|
|
2443
|
-
throw new HostError("unavailable", "
|
|
2719
|
+
throw new HostError("unavailable", this.#hostText("Not available in fullscreen"));
|
|
2444
2720
|
const width = Math.max(318, Number(p.width));
|
|
2445
2721
|
const height = Math.max(120, Number(p.height));
|
|
2446
2722
|
const clamped = width !== p.width || height !== p.height;
|
|
@@ -2460,10 +2736,10 @@ export class TestHost {
|
|
|
2460
2736
|
if (p.on === pip.active)
|
|
2461
2737
|
return;
|
|
2462
2738
|
if (p.on && !pip.supported)
|
|
2463
|
-
throw new HostError("unsupported", "
|
|
2739
|
+
throw new HostError("unsupported", this.#hostText("Picture-in-picture is not supported on this system"));
|
|
2464
2740
|
const video = this.source.phase !== "empty" && this.source.hasVideo !== false;
|
|
2465
2741
|
if (p.on && !video)
|
|
2466
|
-
throw new HostError("unavailable", "
|
|
2742
|
+
throw new HostError("unavailable", this.#hostText("No video"));
|
|
2467
2743
|
this.setMainWindow({
|
|
2468
2744
|
// The player leaves fullscreen before the window opens.
|
|
2469
2745
|
fullscreen: p.on ? false : this.mainWindow.fullscreen,
|
|
@@ -2683,8 +2959,6 @@ export class TestHost {
|
|
|
2683
2959
|
const entry = this.packages.find((each) => each.id === id);
|
|
2684
2960
|
if (!entry)
|
|
2685
2961
|
throw new HostError("missing", `no plugin ${id}`);
|
|
2686
|
-
if (entry.origin === "trial")
|
|
2687
|
-
throw new HostError("unsupported", "a trial is stopped with its handle");
|
|
2688
2962
|
this.packageCalls.push({
|
|
2689
2963
|
method: "setEnabled",
|
|
2690
2964
|
id,
|
|
@@ -2841,7 +3115,8 @@ export class TestHost {
|
|
|
2841
3115
|
};
|
|
2842
3116
|
this.subtitles.tracks.push({
|
|
2843
3117
|
target,
|
|
2844
|
-
title: title ??
|
|
3118
|
+
title: title ??
|
|
3119
|
+
this.#hostText(format === "ass" ? "External ASS" : "External SRT"),
|
|
2845
3120
|
language: null,
|
|
2846
3121
|
styleSupported: true,
|
|
2847
3122
|
styleReason: null,
|
|
@@ -2944,10 +3219,14 @@ export class TestHost {
|
|
|
2944
3219
|
return { jobId: job.jobId, cancel: "requested" };
|
|
2945
3220
|
},
|
|
2946
3221
|
"jobs.operation": () => ({ status: "unknown" }),
|
|
2947
|
-
"sourceResolvers.register": (p) =>
|
|
3222
|
+
"sourceResolvers.register": (p) => {
|
|
3223
|
+
this.#register(p);
|
|
3224
|
+
this.updateFileTypes();
|
|
3225
|
+
},
|
|
2948
3226
|
"sourceResolvers.unregister": (p) => {
|
|
2949
3227
|
this.resolvers.delete(String(p.id));
|
|
2950
3228
|
this.#cancelResolutions();
|
|
3229
|
+
this.updateFileTypes();
|
|
2951
3230
|
},
|
|
2952
3231
|
"sourceResolvers.respond": (p) => this.#respond(p),
|
|
2953
3232
|
"producer.prepare": async () => {
|
|
@@ -3117,6 +3396,8 @@ export class TestHost {
|
|
|
3117
3396
|
*/
|
|
3118
3397
|
export async function startPlugin(activate, options = {}) {
|
|
3119
3398
|
const host = options.host ?? new TestHost();
|
|
3399
|
+
if (options.locale !== undefined)
|
|
3400
|
+
host.locale = options.locale;
|
|
3120
3401
|
const directory = options.directory instanceof URL
|
|
3121
3402
|
? fileURLToPath(options.directory)
|
|
3122
3403
|
: (options.directory ?? process.cwd());
|
|
@@ -3129,6 +3410,7 @@ export async function startPlugin(activate, options = {}) {
|
|
|
3129
3410
|
manifest = { id: read.id, capabilities: read.capabilities };
|
|
3130
3411
|
}
|
|
3131
3412
|
host.manifestId = manifest.id;
|
|
3413
|
+
host.loadCatalog(directory);
|
|
3132
3414
|
const data = options.dataDirectory ?? host.dataDirectory;
|
|
3133
3415
|
mkdirSync(data, { recursive: true });
|
|
3134
3416
|
const instance = await createPlugin(host, manifest, directory, data, host.app);
|