@vestra-player/sdk 1.5.8 → 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 +13 -0
- package/README.md +4 -1
- package/bin/vestra-plugin.mjs +170 -7
- 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 +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/testing.d.ts +133 -3
- package/dist/plugin/testing.js +289 -47
- package/dist/plugin/types.d.ts +194 -20
- package/dist/plugin/ui.js +4 -1
- package/dist/plugin/window.d.ts +2 -0
- package/dist/plugin/window.js +4 -0
- package/dist/wire.d.ts +26 -2
- package/dist/wire.js +12 -2
- package/llms-full.txt +361 -21
- package/llms.txt +43 -39
- 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
|
/**
|
|
@@ -1013,14 +1159,14 @@ export class TestHost {
|
|
|
1013
1159
|
/** Why one frame that way is impossible (B07), or null; the player's rules. */
|
|
1014
1160
|
#frameCheck(forward) {
|
|
1015
1161
|
const s = this.source;
|
|
1016
|
-
const unsupported = "
|
|
1017
|
-
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");
|
|
1018
1164
|
if (s.phase !== "ready" && s.phase !== "ended")
|
|
1019
1165
|
return unsupported;
|
|
1020
1166
|
if (s.kind !== "media")
|
|
1021
1167
|
return forward ? unsupported : noBack;
|
|
1022
1168
|
if (s.hasVideo === false)
|
|
1023
|
-
return "
|
|
1169
|
+
return this.#hostText("No video");
|
|
1024
1170
|
if (forward && s.phase !== "ready")
|
|
1025
1171
|
return unsupported;
|
|
1026
1172
|
if (!forward && !s.capabilities.seek)
|
|
@@ -1031,12 +1177,16 @@ export class TestHost {
|
|
|
1031
1177
|
#coreState(action) {
|
|
1032
1178
|
const f = this.#facts();
|
|
1033
1179
|
const s = this.source;
|
|
1034
|
-
const unsupported = "
|
|
1180
|
+
const unsupported = this.#hostText("Not supported by the current source");
|
|
1035
1181
|
const rule = {
|
|
1036
1182
|
"play-pause": [f.toggle, f.ready ? s.paused : null, unsupported],
|
|
1037
1183
|
stop: [f.close, null, unsupported],
|
|
1038
|
-
previous: [
|
|
1039
|
-
|
|
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")],
|
|
1040
1190
|
"frame-back": [
|
|
1041
1191
|
!this.#frameCheck(false),
|
|
1042
1192
|
null,
|
|
@@ -1061,11 +1211,18 @@ export class TestHost {
|
|
|
1061
1211
|
}
|
|
1062
1212
|
#registerCoreActions() {
|
|
1063
1213
|
const table = [
|
|
1064
|
-
["stop", ["
|
|
1065
|
-
[
|
|
1214
|
+
["stop", ["Close"], 120, "open", ["F4"], false],
|
|
1215
|
+
[
|
|
1216
|
+
"play-pause",
|
|
1217
|
+
["Playback", "Play/Pause"],
|
|
1218
|
+
201,
|
|
1219
|
+
"domain",
|
|
1220
|
+
["Space"],
|
|
1221
|
+
false,
|
|
1222
|
+
],
|
|
1066
1223
|
[
|
|
1067
1224
|
"previous",
|
|
1068
|
-
["
|
|
1225
|
+
["Playback", "Previous"],
|
|
1069
1226
|
202,
|
|
1070
1227
|
"domain",
|
|
1071
1228
|
["PageUp", "Mouse:X1"],
|
|
@@ -1073,17 +1230,31 @@ export class TestHost {
|
|
|
1073
1230
|
],
|
|
1074
1231
|
[
|
|
1075
1232
|
"next",
|
|
1076
|
-
["
|
|
1233
|
+
["Playback", "Next"],
|
|
1077
1234
|
203,
|
|
1078
1235
|
"domain",
|
|
1079
1236
|
["PageDown", "Mouse:X2"],
|
|
1080
1237
|
false,
|
|
1081
1238
|
],
|
|
1082
|
-
[
|
|
1083
|
-
|
|
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
|
+
],
|
|
1084
1255
|
[
|
|
1085
1256
|
"seek-backward",
|
|
1086
|
-
["
|
|
1257
|
+
["Playback", "Seek", "Back 5 s"],
|
|
1087
1258
|
210,
|
|
1088
1259
|
"step",
|
|
1089
1260
|
["ArrowLeft"],
|
|
@@ -1091,7 +1262,7 @@ export class TestHost {
|
|
|
1091
1262
|
],
|
|
1092
1263
|
[
|
|
1093
1264
|
"seek-forward",
|
|
1094
|
-
["
|
|
1265
|
+
["Playback", "Seek", "Forward 5 s"],
|
|
1095
1266
|
211,
|
|
1096
1267
|
"step",
|
|
1097
1268
|
["ArrowRight"],
|
|
@@ -1099,18 +1270,32 @@ export class TestHost {
|
|
|
1099
1270
|
],
|
|
1100
1271
|
[
|
|
1101
1272
|
"rate-normal",
|
|
1102
|
-
["
|
|
1273
|
+
["Playback", "Speed", "Normal"],
|
|
1103
1274
|
220,
|
|
1104
1275
|
"domain",
|
|
1105
1276
|
["Z"],
|
|
1106
1277
|
false,
|
|
1107
1278
|
true,
|
|
1108
1279
|
],
|
|
1109
|
-
[
|
|
1110
|
-
|
|
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
|
+
],
|
|
1111
1296
|
[
|
|
1112
1297
|
"volume-up",
|
|
1113
|
-
["
|
|
1298
|
+
["Audio", "Volume +"],
|
|
1114
1299
|
501,
|
|
1115
1300
|
"domain",
|
|
1116
1301
|
["ArrowUp", "Mouse:WheelUp"],
|
|
@@ -1118,13 +1303,13 @@ export class TestHost {
|
|
|
1118
1303
|
],
|
|
1119
1304
|
[
|
|
1120
1305
|
"volume-down",
|
|
1121
|
-
["
|
|
1306
|
+
["Audio", "Volume −"],
|
|
1122
1307
|
502,
|
|
1123
1308
|
"domain",
|
|
1124
1309
|
["ArrowDown", "Mouse:WheelDown"],
|
|
1125
1310
|
true,
|
|
1126
1311
|
],
|
|
1127
|
-
["mute", ["
|
|
1312
|
+
["mute", ["Audio", "Mute"], 503, "domain", ["M"], false],
|
|
1128
1313
|
];
|
|
1129
1314
|
const host = this;
|
|
1130
1315
|
for (const [id, path, order, group, bindings, repeat, radio] of table) {
|
|
@@ -1138,6 +1323,18 @@ export class TestHost {
|
|
|
1138
1323
|
bindings,
|
|
1139
1324
|
repeat,
|
|
1140
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
|
+
},
|
|
1141
1338
|
get state() {
|
|
1142
1339
|
return host.#coreState(id);
|
|
1143
1340
|
},
|
|
@@ -1175,7 +1372,7 @@ export class TestHost {
|
|
|
1175
1372
|
kind: "control",
|
|
1176
1373
|
control: { kind: "pause", paused: !s.paused },
|
|
1177
1374
|
});
|
|
1178
|
-
this.osd.push(s.phase !== "ended" && !s.paused ? "
|
|
1375
|
+
this.osd.push(this.#hostText(s.phase !== "ended" && !s.paused ? "Paused" : "Playing"));
|
|
1179
1376
|
return;
|
|
1180
1377
|
case "stop":
|
|
1181
1378
|
this.#intent({ kind: "close" });
|
|
@@ -1200,7 +1397,7 @@ export class TestHost {
|
|
|
1200
1397
|
kind: "control",
|
|
1201
1398
|
control: { kind: "seek", seconds: target },
|
|
1202
1399
|
});
|
|
1203
|
-
this.osd.push(
|
|
1400
|
+
this.osd.push(this.#hostText(delta < 0 ? "Back {} s: {} / {}" : "Forward {} s: {} / {}", [Math.abs(delta), time(target), time(s.duration)]));
|
|
1204
1401
|
return;
|
|
1205
1402
|
}
|
|
1206
1403
|
case "rate-normal":
|
|
@@ -1210,7 +1407,7 @@ export class TestHost {
|
|
|
1210
1407
|
? 1
|
|
1211
1408
|
: Math.max(0.25, Math.min(4, Math.round((s.rate + (action === "rate-up" ? 0.1 : -0.1)) * 100) / 100));
|
|
1212
1409
|
this.#intent({ kind: "control", control: { kind: "rate", rate } });
|
|
1213
|
-
this.osd.push(
|
|
1410
|
+
this.osd.push(this.#hostText("Speed: {}x", [rateText(rate)]));
|
|
1214
1411
|
return;
|
|
1215
1412
|
}
|
|
1216
1413
|
case "volume-up":
|
|
@@ -1221,12 +1418,12 @@ export class TestHost {
|
|
|
1221
1418
|
? Math.max(s.output.volume, Math.min(100, Math.round(s.output.volume + 5)))
|
|
1222
1419
|
: Math.max(0, Math.min(200, Math.round(s.output.volume - 5)));
|
|
1223
1420
|
this.setSource({ output: { volume, muted: false } });
|
|
1224
|
-
this.osd.push(
|
|
1421
|
+
this.osd.push(this.#hostText("Volume: {}%", [volume]));
|
|
1225
1422
|
return;
|
|
1226
1423
|
}
|
|
1227
1424
|
case "mute":
|
|
1228
1425
|
this.setSource({ output: { ...s.output, muted: !s.output.muted } });
|
|
1229
|
-
this.osd.push(s.output.muted ? "
|
|
1426
|
+
this.osd.push(this.#hostText(s.output.muted ? "Mute: Off" : "Mute: On"));
|
|
1230
1427
|
return;
|
|
1231
1428
|
}
|
|
1232
1429
|
}
|
|
@@ -1399,6 +1596,8 @@ export class TestHost {
|
|
|
1399
1596
|
pictureInPicture: { ...this.mainWindow.pictureInPicture },
|
|
1400
1597
|
appearance: this.appearance,
|
|
1401
1598
|
fileTypes: this.fileTypes(),
|
|
1599
|
+
language: this.language,
|
|
1600
|
+
locale: this.locale,
|
|
1402
1601
|
};
|
|
1403
1602
|
}
|
|
1404
1603
|
/**
|
|
@@ -1586,10 +1785,14 @@ export class TestHost {
|
|
|
1586
1785
|
const key = this.#bindingKey(id);
|
|
1587
1786
|
const scope = declaration.focus ? "panel" : "global";
|
|
1588
1787
|
const current = this.bindings.overlay.get(key) ?? declaration.bindings;
|
|
1788
|
+
const shown = this.commands.get(id) ?? this.coreCommands.get(id);
|
|
1589
1789
|
return {
|
|
1590
1790
|
...this.#split(key),
|
|
1591
|
-
|
|
1592
|
-
|
|
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,
|
|
1593
1796
|
scope: scope,
|
|
1594
1797
|
defaults: declaration.bindings,
|
|
1595
1798
|
current,
|
|
@@ -1898,8 +2101,10 @@ export class TestHost {
|
|
|
1898
2101
|
for (const other of this.otherResolvers) {
|
|
1899
2102
|
const what = claims(other);
|
|
1900
2103
|
if (what) {
|
|
1901
|
-
const target = what === "host"
|
|
1902
|
-
|
|
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]));
|
|
1903
2108
|
}
|
|
1904
2109
|
}
|
|
1905
2110
|
this.resolvers.set(id, p);
|
|
@@ -2026,8 +2231,12 @@ export class TestHost {
|
|
|
2026
2231
|
this.commands.size + this.coreCommands.size >= COMMAND_LIMITS.total)
|
|
2027
2232
|
throw new HostError("limit", "command registry limit reached");
|
|
2028
2233
|
const host = this;
|
|
2234
|
+
const declaration = p.declaration;
|
|
2029
2235
|
this.commands.set(id, {
|
|
2030
|
-
declaration
|
|
2236
|
+
declaration,
|
|
2237
|
+
get display() {
|
|
2238
|
+
return host.#display(declaration.path ?? [], declaration.title);
|
|
2239
|
+
},
|
|
2031
2240
|
state: p.state,
|
|
2032
2241
|
revision: Number(p.revision),
|
|
2033
2242
|
async run() {
|
|
@@ -2102,10 +2311,15 @@ export class TestHost {
|
|
|
2102
2311
|
? this.layout.bottom?.panels?.[0]?.plugin
|
|
2103
2312
|
: undefined;
|
|
2104
2313
|
if (holder !== undefined || (p.slot === "bottom" && this.layout.bottom))
|
|
2105
|
-
throw taken(
|
|
2314
|
+
throw taken(this.#hostText("The {} is used by {}", [
|
|
2315
|
+
this.#hostText(SLOT_LABELS[String(p.slot)]),
|
|
2316
|
+
String(holder),
|
|
2317
|
+
]));
|
|
2106
2318
|
if (p.slot === "sidebar" &&
|
|
2107
2319
|
(this.layout.sidebar?.panels?.length ?? 0) >= MAX_SIDEBAR_TABS)
|
|
2108
|
-
throw taken(
|
|
2320
|
+
throw taken(this.#hostText("The sidebar holds at most {} tabs", [
|
|
2321
|
+
MAX_SIDEBAR_TABS,
|
|
2322
|
+
]));
|
|
2109
2323
|
this.#seeImages(p.properties);
|
|
2110
2324
|
const host = this;
|
|
2111
2325
|
this.panel = {
|
|
@@ -2131,7 +2345,9 @@ export class TestHost {
|
|
|
2131
2345
|
this.#addPanel(slot, {
|
|
2132
2346
|
owner: 1,
|
|
2133
2347
|
plugin: this.manifestId,
|
|
2134
|
-
title: typeof p.title === "string" && p.title
|
|
2348
|
+
title: typeof p.title === "string" && p.title
|
|
2349
|
+
? this.translate(p.title)
|
|
2350
|
+
: this.manifestId,
|
|
2135
2351
|
});
|
|
2136
2352
|
return { status: "ready", prepareMs: 0, shellGeneration: 1 };
|
|
2137
2353
|
},
|
|
@@ -2184,12 +2400,15 @@ export class TestHost {
|
|
|
2184
2400
|
state.detached = p.detached;
|
|
2185
2401
|
},
|
|
2186
2402
|
"osd.show": (p) => {
|
|
2187
|
-
const text = String(p.text);
|
|
2188
|
-
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)
|
|
2189
2405
|
throw new HostError("invalid", "osd text needs 1..200 characters");
|
|
2190
2406
|
this.osd.push(text);
|
|
2191
2407
|
},
|
|
2192
|
-
"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)),
|
|
2193
2412
|
"playback.stats": () => this.stats,
|
|
2194
2413
|
"audio.addTrack": (p) => {
|
|
2195
2414
|
this.#audioCondition(p.expected);
|
|
@@ -2292,7 +2511,7 @@ export class TestHost {
|
|
|
2292
2511
|
const reason = p.unit === "keyframe"
|
|
2293
2512
|
? s.phase === "ready" && s.capabilities.seek && s.hasVideo !== false
|
|
2294
2513
|
? null
|
|
2295
|
-
: "
|
|
2514
|
+
: this.#hostText("Not supported by the current source")
|
|
2296
2515
|
: this.#frameCheck(p.direction === "forward");
|
|
2297
2516
|
if (reason)
|
|
2298
2517
|
throw new HostError("unsupported", reason);
|
|
@@ -2339,6 +2558,14 @@ export class TestHost {
|
|
|
2339
2558
|
this.dropProviders.splice(index, 1);
|
|
2340
2559
|
},
|
|
2341
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
|
+
},
|
|
2342
2569
|
"appearance.set": (p) => {
|
|
2343
2570
|
// Like the player: an id it does not have selects the default (the
|
|
2344
2571
|
// SDK's own `set` refuses one before it gets here).
|
|
@@ -2356,7 +2583,7 @@ export class TestHost {
|
|
|
2356
2583
|
const host = this;
|
|
2357
2584
|
this.settings.pages.set(id, {
|
|
2358
2585
|
category: String(p.category),
|
|
2359
|
-
title: String(p.title),
|
|
2586
|
+
title: this.translate(String(p.title)),
|
|
2360
2587
|
order: Number(p.order ?? 0),
|
|
2361
2588
|
file: String(p.file),
|
|
2362
2589
|
component: String(p.component),
|
|
@@ -2468,6 +2695,17 @@ export class TestHost {
|
|
|
2468
2695
|
"ui.interaction": (p) => void (this.mounted().locks = p),
|
|
2469
2696
|
"dialogs.confirm": (p) => {
|
|
2470
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
|
+
});
|
|
2471
2709
|
const answer = this.dialogAnswers.shift();
|
|
2472
2710
|
if (answer === undefined)
|
|
2473
2711
|
throw new Error("test host: no dialog answer queued");
|
|
@@ -2478,7 +2716,7 @@ export class TestHost {
|
|
|
2478
2716
|
"window.state": () => this.#windowFacts(),
|
|
2479
2717
|
"window.setVideoArea": (p) => {
|
|
2480
2718
|
if (this.mainWindow.fullscreen)
|
|
2481
|
-
throw new HostError("unavailable", "
|
|
2719
|
+
throw new HostError("unavailable", this.#hostText("Not available in fullscreen"));
|
|
2482
2720
|
const width = Math.max(318, Number(p.width));
|
|
2483
2721
|
const height = Math.max(120, Number(p.height));
|
|
2484
2722
|
const clamped = width !== p.width || height !== p.height;
|
|
@@ -2498,10 +2736,10 @@ export class TestHost {
|
|
|
2498
2736
|
if (p.on === pip.active)
|
|
2499
2737
|
return;
|
|
2500
2738
|
if (p.on && !pip.supported)
|
|
2501
|
-
throw new HostError("unsupported", "
|
|
2739
|
+
throw new HostError("unsupported", this.#hostText("Picture-in-picture is not supported on this system"));
|
|
2502
2740
|
const video = this.source.phase !== "empty" && this.source.hasVideo !== false;
|
|
2503
2741
|
if (p.on && !video)
|
|
2504
|
-
throw new HostError("unavailable", "
|
|
2742
|
+
throw new HostError("unavailable", this.#hostText("No video"));
|
|
2505
2743
|
this.setMainWindow({
|
|
2506
2744
|
// The player leaves fullscreen before the window opens.
|
|
2507
2745
|
fullscreen: p.on ? false : this.mainWindow.fullscreen,
|
|
@@ -2877,7 +3115,8 @@ export class TestHost {
|
|
|
2877
3115
|
};
|
|
2878
3116
|
this.subtitles.tracks.push({
|
|
2879
3117
|
target,
|
|
2880
|
-
title: title ??
|
|
3118
|
+
title: title ??
|
|
3119
|
+
this.#hostText(format === "ass" ? "External ASS" : "External SRT"),
|
|
2881
3120
|
language: null,
|
|
2882
3121
|
styleSupported: true,
|
|
2883
3122
|
styleReason: null,
|
|
@@ -3157,6 +3396,8 @@ export class TestHost {
|
|
|
3157
3396
|
*/
|
|
3158
3397
|
export async function startPlugin(activate, options = {}) {
|
|
3159
3398
|
const host = options.host ?? new TestHost();
|
|
3399
|
+
if (options.locale !== undefined)
|
|
3400
|
+
host.locale = options.locale;
|
|
3160
3401
|
const directory = options.directory instanceof URL
|
|
3161
3402
|
? fileURLToPath(options.directory)
|
|
3162
3403
|
: (options.directory ?? process.cwd());
|
|
@@ -3169,6 +3410,7 @@ export async function startPlugin(activate, options = {}) {
|
|
|
3169
3410
|
manifest = { id: read.id, capabilities: read.capabilities };
|
|
3170
3411
|
}
|
|
3171
3412
|
host.manifestId = manifest.id;
|
|
3413
|
+
host.loadCatalog(directory);
|
|
3172
3414
|
const data = options.dataDirectory ?? host.dataDirectory;
|
|
3173
3415
|
mkdirSync(data, { recursive: true });
|
|
3174
3416
|
const instance = await createPlugin(host, manifest, directory, data, host.app);
|