@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
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/** The player's palette (`plugin.appearance`; `ui.theme` to change it). */
|
|
2
2
|
import { failure } from "./runtime.js";
|
|
3
3
|
import { windowOf } from "./window.js";
|
|
4
|
+
import { LOCALES } from "../i18n.js";
|
|
5
|
+
const preference = (value) => value === "system" || LOCALES.includes(value)
|
|
6
|
+
? value
|
|
7
|
+
: undefined;
|
|
4
8
|
/** The palettes this SDK knows, the default first. */
|
|
5
9
|
export const THEMES = ["vestra", "vestra-light"];
|
|
6
10
|
/**
|
|
@@ -11,19 +15,28 @@ export const THEMES = ["vestra", "vestra-light"];
|
|
|
11
15
|
class AppearanceService {
|
|
12
16
|
#runtime;
|
|
13
17
|
theme;
|
|
18
|
+
/** The saved interface language (the next start's). */
|
|
19
|
+
language;
|
|
14
20
|
#listeners = new Set();
|
|
15
21
|
#started = false;
|
|
16
22
|
constructor(runtime) {
|
|
17
23
|
this.#runtime = runtime;
|
|
18
24
|
this.theme =
|
|
19
25
|
windowOf(runtime).appearance ?? "vestra";
|
|
26
|
+
this.language = preference(windowOf(runtime).language) ?? "system";
|
|
20
27
|
}
|
|
21
28
|
start() {
|
|
22
29
|
if (this.#started)
|
|
23
30
|
return;
|
|
24
31
|
this.#started = true;
|
|
25
32
|
this.#runtime.on("appearance.changed", (data) => {
|
|
26
|
-
const
|
|
33
|
+
const event = data;
|
|
34
|
+
const language = preference(event?.language);
|
|
35
|
+
if (language && language !== this.language) {
|
|
36
|
+
this.language = language;
|
|
37
|
+
this.#runtime.invalidate();
|
|
38
|
+
}
|
|
39
|
+
const theme = event?.theme;
|
|
27
40
|
if (theme && THEMES.includes(theme))
|
|
28
41
|
this.accept(theme);
|
|
29
42
|
});
|
|
@@ -65,5 +78,18 @@ export function appearance(scope) {
|
|
|
65
78
|
await scope.runtime.host.appearance.set(theme);
|
|
66
79
|
service().accept(theme);
|
|
67
80
|
},
|
|
81
|
+
get language() {
|
|
82
|
+
service().start();
|
|
83
|
+
return service().language;
|
|
84
|
+
},
|
|
85
|
+
async setLanguage(language) {
|
|
86
|
+
if (!preference(language))
|
|
87
|
+
throw failure("invalid", `language is "system" or one of ${LOCALES.map((l) => `"${l}"`).join(", ")}`);
|
|
88
|
+
await scope.runtime.host.appearance.setLanguage(language);
|
|
89
|
+
if (service().language !== language) {
|
|
90
|
+
service().language = language;
|
|
91
|
+
scope.runtime.invalidate();
|
|
92
|
+
}
|
|
93
|
+
},
|
|
68
94
|
};
|
|
69
95
|
}
|
package/dist/plugin/osd.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { ScopeImpl } from "./scope.ts";
|
|
2
2
|
import type { Osd } from "./types.ts";
|
|
3
|
+
import type { TemplateArgsJson } from "../wire.ts";
|
|
4
|
+
/** Copy and check template arguments: an array or object of ≤ 16 strings and numbers. */
|
|
5
|
+
export declare function templateArgs(args: unknown): TemplateArgsJson | undefined;
|
|
3
6
|
export declare const osd: (scope: ScopeImpl) => Osd;
|
package/dist/plugin/osd.js
CHANGED
|
@@ -1,23 +1,59 @@
|
|
|
1
1
|
/** On-screen display: a transient line and a persistent information block. */
|
|
2
2
|
import { Latest } from "./runtime.js";
|
|
3
|
+
import { failure } from "./runtime.js";
|
|
4
|
+
/** Copy and check template arguments: an array or object of ≤ 16 strings and numbers. */
|
|
5
|
+
export function templateArgs(args) {
|
|
6
|
+
if (args === undefined || args === null)
|
|
7
|
+
return undefined;
|
|
8
|
+
const values = Array.isArray(args)
|
|
9
|
+
? args
|
|
10
|
+
: typeof args === "object"
|
|
11
|
+
? Object.values(args)
|
|
12
|
+
: null;
|
|
13
|
+
if (!values ||
|
|
14
|
+
values.length > 16 ||
|
|
15
|
+
values.some((value) => !((typeof value === "number" && Number.isFinite(value)) ||
|
|
16
|
+
typeof value === "string")))
|
|
17
|
+
throw failure("invalid", "args is an array or object of at most 16 strings and finite numbers");
|
|
18
|
+
return Array.isArray(args)
|
|
19
|
+
? [...args]
|
|
20
|
+
: { ...args };
|
|
21
|
+
}
|
|
3
22
|
class PanelHandle {
|
|
4
23
|
#scope;
|
|
5
24
|
#pump;
|
|
6
|
-
|
|
25
|
+
/** The last value sent, as JSON (`null` text: hidden). */
|
|
26
|
+
#last = JSON.stringify({ text: null });
|
|
7
27
|
#disposed = false;
|
|
8
28
|
constructor(scope) {
|
|
9
29
|
if (scope.runtime.services.has("osd.panel"))
|
|
10
30
|
throw new Error("a plugin can have only one osd panel");
|
|
11
31
|
scope.runtime.services.set("osd.panel", this);
|
|
12
32
|
this.#scope = scope;
|
|
13
|
-
this.#pump = new Latest((text) => scope.runtime.host.osd.panel(text));
|
|
33
|
+
this.#pump = new Latest(({ text, args }) => scope.runtime.host.osd.panel(text, args));
|
|
14
34
|
scope.add(this);
|
|
15
35
|
}
|
|
16
|
-
set(text) {
|
|
17
|
-
if (this.#disposed
|
|
36
|
+
set(text, args) {
|
|
37
|
+
if (this.#disposed)
|
|
38
|
+
return;
|
|
39
|
+
let value;
|
|
40
|
+
try {
|
|
41
|
+
value = {
|
|
42
|
+
text,
|
|
43
|
+
...(text === null || args === undefined
|
|
44
|
+
? {}
|
|
45
|
+
: { args: templateArgs(args) }),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
this.#scope.report(error);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const key = JSON.stringify(value);
|
|
53
|
+
if (key === this.#last)
|
|
18
54
|
return;
|
|
19
|
-
this.#last =
|
|
20
|
-
this.#pump.push(
|
|
55
|
+
this.#last = key;
|
|
56
|
+
this.#pump.push(value).catch((error) => this.#scope.report(error));
|
|
21
57
|
}
|
|
22
58
|
async dispose() {
|
|
23
59
|
if (this.#disposed)
|
|
@@ -25,12 +61,12 @@ class PanelHandle {
|
|
|
25
61
|
this.#disposed = true;
|
|
26
62
|
this.#scope.forget(this);
|
|
27
63
|
this.#scope.runtime.services.delete("osd.panel");
|
|
28
|
-
if (this.#last !== null)
|
|
29
|
-
await this.#pump.push(null).catch(() => { });
|
|
64
|
+
if (this.#last !== JSON.stringify({ text: null }))
|
|
65
|
+
await this.#pump.push({ text: null }).catch(() => { });
|
|
30
66
|
this.#pump.stop();
|
|
31
67
|
}
|
|
32
68
|
}
|
|
33
69
|
export const osd = (scope) => ({
|
|
34
|
-
show: (text) => scope.runtime.host.osd.show(text),
|
|
70
|
+
show: async (text, args) => scope.runtime.host.osd.show(text, templateArgs(args)),
|
|
35
71
|
panel: () => new PanelHandle(scope),
|
|
36
72
|
});
|
package/dist/plugin/packages.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Package validation
|
|
1
|
+
/** Package validation and install/rollback (`plugins.manage`). */
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { failure, sleep, uncertain } from "./runtime.js";
|
|
@@ -9,51 +9,6 @@ const TERMINAL = [
|
|
|
9
9
|
"failedDegraded",
|
|
10
10
|
"tooLate",
|
|
11
11
|
];
|
|
12
|
-
const STARTING = ["preparing", "activating", "starting"];
|
|
13
|
-
class TrialHandle {
|
|
14
|
-
#scope;
|
|
15
|
-
#record;
|
|
16
|
-
#stopped;
|
|
17
|
-
constructor(scope, record) {
|
|
18
|
-
this.#scope = scope;
|
|
19
|
-
this.#record = record;
|
|
20
|
-
scope.add(this);
|
|
21
|
-
}
|
|
22
|
-
get status() {
|
|
23
|
-
return this.#record.status;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Concurrent calls share one stop; a confirmed stop is final. A failed one
|
|
27
|
-
* (code `cleanup`: the host did not confirm cleanup) is forgotten, so the
|
|
28
|
-
* next call sends a new stop to the host.
|
|
29
|
-
*/
|
|
30
|
-
stop() {
|
|
31
|
-
const host = this.#scope.runtime.host.packages;
|
|
32
|
-
if (this.#stopped)
|
|
33
|
-
return this.#stopped;
|
|
34
|
-
const stopping = (async () => {
|
|
35
|
-
this.#record = await host.stopTrial(this.#record.trialId);
|
|
36
|
-
while (!this.#record.cleanupConfirmed &&
|
|
37
|
-
this.#record.status !== "stopped" &&
|
|
38
|
-
this.#record.status !== "failed") {
|
|
39
|
-
await sleep(250);
|
|
40
|
-
this.#record = await host.trialState(this.#record.trialId);
|
|
41
|
-
}
|
|
42
|
-
if (!this.#record.cleanupConfirmed)
|
|
43
|
-
throw failure("cleanup", `trial cleanup not confirmed: ${this.#record.status}`);
|
|
44
|
-
})();
|
|
45
|
-
this.#stopped = stopping;
|
|
46
|
-
stopping.catch(() => {
|
|
47
|
-
if (this.#stopped === stopping)
|
|
48
|
-
this.#stopped = undefined;
|
|
49
|
-
});
|
|
50
|
-
return stopping;
|
|
51
|
-
}
|
|
52
|
-
async dispose() {
|
|
53
|
-
this.#scope.forget(this);
|
|
54
|
-
await this.stop().catch(() => { });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
12
|
class ChangeHandle {
|
|
58
13
|
#scope;
|
|
59
14
|
#id;
|
|
@@ -123,24 +78,6 @@ async function change(scope, submit, id) {
|
|
|
123
78
|
}
|
|
124
79
|
return new ChangeHandle(scope, operation);
|
|
125
80
|
}
|
|
126
|
-
async function trial(scope, runtime, token) {
|
|
127
|
-
const host = runtime.host.packages;
|
|
128
|
-
const requestId = randomUUID();
|
|
129
|
-
let record;
|
|
130
|
-
try {
|
|
131
|
-
record = await host.trial(token, requestId);
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
if (!uncertain(error))
|
|
135
|
-
throw error;
|
|
136
|
-
record = await host.trialStateByRequest(requestId);
|
|
137
|
-
}
|
|
138
|
-
while (STARTING.includes(record.status)) {
|
|
139
|
-
await sleep(250);
|
|
140
|
-
record = await host.trialState(record.trialId);
|
|
141
|
-
}
|
|
142
|
-
return new TrialHandle(scope, record);
|
|
143
|
-
}
|
|
144
81
|
function installOptions(options) {
|
|
145
82
|
if (options === undefined)
|
|
146
83
|
return {};
|
|
@@ -171,7 +108,6 @@ function candidate(scope, directory, checked) {
|
|
|
171
108
|
digest: checked.digest,
|
|
172
109
|
manifest: checked.manifest,
|
|
173
110
|
checks: checked.checks,
|
|
174
|
-
trial: () => trial(scope, runtime, checked.token),
|
|
175
111
|
install: async (options) => {
|
|
176
112
|
const wire = installOptions(options);
|
|
177
113
|
return change(scope, (requestId, revision) => runtime.host.packages.install(checked.token, revision, requestId, wire), checked.manifest.id);
|
|
@@ -225,4 +161,14 @@ const info = (plugin) => Object.freeze({
|
|
|
225
161
|
...(warning.slot === undefined ? {} : { slot: warning.slot }),
|
|
226
162
|
...(warning.owner === undefined ? {} : { owner: warning.owner }),
|
|
227
163
|
}))),
|
|
164
|
+
...(plugin.display
|
|
165
|
+
? {
|
|
166
|
+
display: Object.freeze({
|
|
167
|
+
name: plugin.display.name ?? null,
|
|
168
|
+
punchline: plugin.display.punchline ?? null,
|
|
169
|
+
description: plugin.display.description ?? null,
|
|
170
|
+
tags: Object.freeze([...(plugin.display.tags ?? [])]),
|
|
171
|
+
}),
|
|
172
|
+
}
|
|
173
|
+
: {}),
|
|
228
174
|
});
|
package/dist/plugin/resolvers.js
CHANGED
|
@@ -5,8 +5,72 @@
|
|
|
5
5
|
import { failure, quiet } from "./runtime.js";
|
|
6
6
|
import { playerOf } from "./player.js";
|
|
7
7
|
import { resolvedStream } from "./stream.js";
|
|
8
|
+
import { windowOf } from "./window.js";
|
|
8
9
|
const isFiles = (options) => "extensions" in options;
|
|
10
|
+
const sameTypes = (a, b) => a.length === b.length && a.every((type, i) => type === b[i]);
|
|
11
|
+
/**
|
|
12
|
+
* One per plugin: the list comes with the `window.state` read before
|
|
13
|
+
* activation (`[]` from older players) and is read again when the plugin
|
|
14
|
+
* first uses it; with `events`, `resolvers.fileTypes` keeps it current.
|
|
15
|
+
*/
|
|
16
|
+
class FileTypesService {
|
|
17
|
+
#runtime;
|
|
18
|
+
types;
|
|
19
|
+
#listeners = new Set();
|
|
20
|
+
#started = false;
|
|
21
|
+
constructor(runtime) {
|
|
22
|
+
this.#runtime = runtime;
|
|
23
|
+
this.types = Object.freeze([...(windowOf(runtime).fileTypes ?? [])]);
|
|
24
|
+
}
|
|
25
|
+
start() {
|
|
26
|
+
if (this.#started)
|
|
27
|
+
return;
|
|
28
|
+
this.#started = true;
|
|
29
|
+
this.#runtime.on("resolvers.fileTypes", (data) => {
|
|
30
|
+
const types = data?.fileTypes;
|
|
31
|
+
if (Array.isArray(types) && types.every((t) => typeof t === "string"))
|
|
32
|
+
this.accept(types);
|
|
33
|
+
});
|
|
34
|
+
// What changed between the start-up read and the subscription.
|
|
35
|
+
const window = windowOf(this.#runtime);
|
|
36
|
+
void window.read().then(() => {
|
|
37
|
+
if (window.fileTypes)
|
|
38
|
+
this.accept(window.fileTypes, this.#runtime.can("events"));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** `notify`: false without `events`, whose listeners never fire. */
|
|
42
|
+
accept(types, notify = true) {
|
|
43
|
+
if (sameTypes(types, this.types))
|
|
44
|
+
return;
|
|
45
|
+
this.types = Object.freeze([...types]);
|
|
46
|
+
if (notify)
|
|
47
|
+
for (const listener of [...this.#listeners])
|
|
48
|
+
listener([...types]);
|
|
49
|
+
this.#runtime.invalidate();
|
|
50
|
+
}
|
|
51
|
+
watch(listener) {
|
|
52
|
+
this.#listeners.add(listener);
|
|
53
|
+
return { dispose: async () => void this.#listeners.delete(listener) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const fileTypesOf = (runtime) => runtime.service("fileTypes", () => new FileTypesService(runtime));
|
|
9
57
|
export const resolvers = (scope) => ({
|
|
58
|
+
get fileTypes() {
|
|
59
|
+
const service = fileTypesOf(scope.runtime);
|
|
60
|
+
service.start();
|
|
61
|
+
return service.types;
|
|
62
|
+
},
|
|
63
|
+
onFileTypesChange(listener) {
|
|
64
|
+
const service = fileTypesOf(scope.runtime);
|
|
65
|
+
service.start();
|
|
66
|
+
const watch = service.watch((types) => scope.run(listener, types));
|
|
67
|
+
return scope.add({
|
|
68
|
+
dispose: async () => {
|
|
69
|
+
scope.forget(watch);
|
|
70
|
+
await watch.dispose();
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
},
|
|
10
74
|
add(options) {
|
|
11
75
|
const runtime = scope.runtime;
|
|
12
76
|
const registration = isFiles(options)
|
package/dist/plugin/runtime.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Json, LocalServices, Transport, Wire } from "../wire.ts";
|
|
2
|
-
import type { App, Disposable, PluginError, SystemInfo } from "./types.ts";
|
|
2
|
+
import type { App, Disposable, Locale, PluginError, SystemInfo } from "./types.ts";
|
|
3
3
|
export interface PluginManifest {
|
|
4
4
|
id: string;
|
|
5
5
|
capabilities: string[];
|
|
@@ -40,6 +40,8 @@ export declare class Runtime {
|
|
|
40
40
|
readonly services: Map<string, unknown>;
|
|
41
41
|
/** `plugin.app`, from the start-up handshake; `system()` asks the player. */
|
|
42
42
|
readonly app: App;
|
|
43
|
+
/** `plugin.i18n.locale`, from the start-up handshake; English when unknown. */
|
|
44
|
+
readonly locale: Locale;
|
|
43
45
|
constructor(transport: Transport, manifest: PluginManifest, directory: string, dataDirectory?: string, app?: unknown);
|
|
44
46
|
can(capability: string): boolean;
|
|
45
47
|
/** Lazily created singletons shared by every scope of this plugin. */
|
package/dist/plugin/runtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared machinery behind every scope: invocation, update passes, events. */
|
|
2
2
|
import { createWire, HostError } from "../wire.js";
|
|
3
|
+
import { LOCALES } from "../i18n.js";
|
|
3
4
|
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
5
|
export function toError(error) {
|
|
5
6
|
if (error instanceof Error) {
|
|
@@ -94,6 +95,8 @@ export class Runtime {
|
|
|
94
95
|
services = new Map();
|
|
95
96
|
/** `plugin.app`, from the start-up handshake; `system()` asks the player. */
|
|
96
97
|
app;
|
|
98
|
+
/** `plugin.i18n.locale`, from the start-up handshake; English when unknown. */
|
|
99
|
+
locale;
|
|
97
100
|
constructor(transport, manifest, directory, dataDirectory, app) {
|
|
98
101
|
this.#transport = transport;
|
|
99
102
|
this.dataDirectory = dataDirectory;
|
|
@@ -101,6 +104,9 @@ export class Runtime {
|
|
|
101
104
|
this.host = createWire(transport);
|
|
102
105
|
const host = this.host;
|
|
103
106
|
this.app = appInfo(app, () => host.app.system());
|
|
107
|
+
this.locale = LOCALES.includes(transport.locale ?? "")
|
|
108
|
+
? transport.locale
|
|
109
|
+
: "en";
|
|
104
110
|
this.manifest = manifest;
|
|
105
111
|
this.directory = directory;
|
|
106
112
|
transport.on("ui.callback", (data) => {
|
package/dist/plugin/scope.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare class ScopeImpl implements Plugin {
|
|
|
39
39
|
readonly drops: import("./types.ts").Drops;
|
|
40
40
|
readonly appearance: import("./types.ts").Appearance;
|
|
41
41
|
readonly settings: import("./types.ts").Settings;
|
|
42
|
+
readonly i18n: import("./types.ts").I18n;
|
|
42
43
|
get app(): import("./types.ts").App;
|
|
43
44
|
scope(): Scope;
|
|
44
45
|
add<T extends Disposable>(item: T): T;
|
package/dist/plugin/scope.js
CHANGED
|
@@ -6,6 +6,7 @@ import { timeline } from "./timeline.js";
|
|
|
6
6
|
import { drops } from "./drops.js";
|
|
7
7
|
import { appearance } from "./appearance.js";
|
|
8
8
|
import { settings } from "./settings.js";
|
|
9
|
+
import { i18n } from "./language.js";
|
|
9
10
|
import { player } from "./player.js";
|
|
10
11
|
import { audioFilters, videoFilters } from "./filters.js";
|
|
11
12
|
import { videoRender, videoView } from "./view.js";
|
|
@@ -69,6 +70,7 @@ export class ScopeImpl {
|
|
|
69
70
|
drops = drops(this);
|
|
70
71
|
appearance = appearance(this);
|
|
71
72
|
settings = settings(this);
|
|
73
|
+
i18n = i18n(this);
|
|
72
74
|
get app() {
|
|
73
75
|
return this.runtime.app;
|
|
74
76
|
}
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -8,11 +8,28 @@ import type { SourceState } from "../source.ts";
|
|
|
8
8
|
import type { SubtitleState } from "../subtitles.ts";
|
|
9
9
|
import type { RenderSettings, ViewSettings } from "./types.ts";
|
|
10
10
|
import type { MediaJob } from "../media.ts";
|
|
11
|
-
import type { Plugin, ThemeId } from "./types.ts";
|
|
11
|
+
import type { LanguagePreference, Locale, Plugin, TemplateArgs, ThemeId } from "./types.ts";
|
|
12
|
+
import { LOCALES } from "../i18n.ts";
|
|
12
13
|
/** Throw one of these from `host.handle(...)` to simulate a definite refusal. */
|
|
13
14
|
export { HostError };
|
|
14
15
|
/** The player's command caps, which the test host enforces as well. */
|
|
15
16
|
export { COMMAND_LIMITS, MENU_IDS };
|
|
17
|
+
/**
|
|
18
|
+
* The player's interface languages, English (the source) first, as the
|
|
19
|
+
* player's own table: `["en", "zh-Hans", "ja", "ko", "de", "fr", "es"]`.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { LOCALES } from "@vestra-player/sdk/testing";
|
|
24
|
+
*
|
|
25
|
+
* for (const locale of LOCALES.slice(1)) {
|
|
26
|
+
* const { host, stop } = await startPlugin(activate, { directory, locale });
|
|
27
|
+
* assert.notEqual(host.command("open").display.title, "Open…");
|
|
28
|
+
* await stop();
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export { LOCALES };
|
|
16
33
|
type Handler = (params: Record<string, Json>) => unknown;
|
|
17
34
|
type Call = {
|
|
18
35
|
method: string;
|
|
@@ -80,6 +97,20 @@ export interface FakeCommand {
|
|
|
80
97
|
* versions.
|
|
81
98
|
*/
|
|
82
99
|
declaration: CommandDeclaration;
|
|
100
|
+
/**
|
|
101
|
+
* What the menu shows, as the player shows it in the test host's
|
|
102
|
+
* {@link TestHost.locale}: the title and each path segment translated with
|
|
103
|
+
* the plugin's catalog (the player's own menus in the player's words), the
|
|
104
|
+
* keys normalized (`@video` is "Video").
|
|
105
|
+
*
|
|
106
|
+
* @since 1.5.9
|
|
107
|
+
*/
|
|
108
|
+
readonly display: {
|
|
109
|
+
/** The menu text of the item. */
|
|
110
|
+
title: string;
|
|
111
|
+
/** Each segment of the path as shown; empty without a path. */
|
|
112
|
+
path: string[];
|
|
113
|
+
};
|
|
83
114
|
/** The command state the player last received (after the update pass). */
|
|
84
115
|
state: {
|
|
85
116
|
/** `false` when the plugin disabled the command. */
|
|
@@ -257,10 +288,101 @@ export declare class TestHost implements Transport {
|
|
|
257
288
|
*/
|
|
258
289
|
sidebar: LayoutSlot | null;
|
|
259
290
|
};
|
|
260
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* Every `osd.show` text as the player shows it (translated with the
|
|
293
|
+
* plugin's catalog and formatted with its args), newest last. The
|
|
294
|
+
* template and args the plugin sent are in `host.sent("osd.show")`.
|
|
295
|
+
*/
|
|
261
296
|
readonly osd: string[];
|
|
262
|
-
/** The text of this plugin's osd panel, `null` when hidden. */
|
|
297
|
+
/** The text of this plugin's osd panel as shown, `null` when hidden. */
|
|
263
298
|
osdPanel: string | null;
|
|
299
|
+
/**
|
|
300
|
+
* The interface language the test host plays (`plugin.i18n.locale`), `"en"`
|
|
301
|
+
* by default. Set it before {@link startPlugin} (or pass its `locale`
|
|
302
|
+
* option): the plugin's catalog for it is read from the package's
|
|
303
|
+
* `locales/` then, and the menus ({@link FakeCommand.display}), OSD
|
|
304
|
+
* messages, dialog texts, settings page and panel titles show its
|
|
305
|
+
* translations, the way the player shows them.
|
|
306
|
+
*
|
|
307
|
+
* The player's own texts (core actions' messages and menus, its
|
|
308
|
+
* refusals) are English, or the player's Simplified Chinese for
|
|
309
|
+
* `"zh-Hans"`. While `locale` was never set they stay in Chinese, as
|
|
310
|
+
* players up to 0.4.2 showed them, so tests written for SDK 1.5.8 keep
|
|
311
|
+
* passing; set `locale` to test what a user sees now.
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```ts
|
|
315
|
+
* const { host, stop } = await startPlugin(activate, { directory, locale: "zh-Hans" });
|
|
316
|
+
* assert.deepEqual(host.command("blur").display.path, ["视频", "模糊…"]);
|
|
317
|
+
* await stop();
|
|
318
|
+
* ```
|
|
319
|
+
*
|
|
320
|
+
* @since 1.5.9
|
|
321
|
+
*/
|
|
322
|
+
get locale(): Locale;
|
|
323
|
+
set locale(locale: Locale);
|
|
324
|
+
/**
|
|
325
|
+
* Read the plugin's translations for {@link TestHost.locale} from a
|
|
326
|
+
* package directory, as the player would (`locales/<locale>.json` of a
|
|
327
|
+
* built package, else the source `locales/<locale>.po`); `startPlugin`
|
|
328
|
+
* does this. None for English; a missing file means none; a broken `.po`
|
|
329
|
+
* throws.
|
|
330
|
+
*
|
|
331
|
+
* @param directory - The package directory (holding `locales/`).
|
|
332
|
+
* @since 1.5.9
|
|
333
|
+
*/
|
|
334
|
+
loadCatalog(directory: string): void;
|
|
335
|
+
/**
|
|
336
|
+
* A text of the plugin as the player shows it: translated with the
|
|
337
|
+
* plugin's catalog ({@link TestHost.loadCatalog}), then formatted with
|
|
338
|
+
* `args` when given.
|
|
339
|
+
*
|
|
340
|
+
* @param text - English source text or template.
|
|
341
|
+
* @param args - Values for its placeholders.
|
|
342
|
+
* @returns The shown text.
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* ```ts
|
|
346
|
+
* assert.equal(host.translate("Speed {}×", [2]), "倍速 2×");
|
|
347
|
+
* ```
|
|
348
|
+
*
|
|
349
|
+
* @since 1.5.9
|
|
350
|
+
*/
|
|
351
|
+
translate(text: string, args?: TemplateArgs): string;
|
|
352
|
+
/**
|
|
353
|
+
* The saved interface language (`plugin.appearance.language`), changed by
|
|
354
|
+
* `appearance.setLanguage` or {@link TestHost.setLanguage}.
|
|
355
|
+
*
|
|
356
|
+
* @since 1.5.9
|
|
357
|
+
*/
|
|
358
|
+
language: LanguagePreference;
|
|
359
|
+
/**
|
|
360
|
+
* Simulate another plugin (or the user) saving the interface language:
|
|
361
|
+
* pushes `appearance.changed`.
|
|
362
|
+
*
|
|
363
|
+
* @param language - `"system"` or a locale.
|
|
364
|
+
* @since 1.5.9
|
|
365
|
+
*/
|
|
366
|
+
setLanguage(language: LanguagePreference): void;
|
|
367
|
+
/**
|
|
368
|
+
* Every `dialogs.confirm` as the player shows it: its texts translated
|
|
369
|
+
* (message and detail formatted with `args`), newest last. The options
|
|
370
|
+
* the plugin sent are in {@link TestHost.confirms}.
|
|
371
|
+
*
|
|
372
|
+
* @since 1.5.9
|
|
373
|
+
*/
|
|
374
|
+
readonly confirmTexts: {
|
|
375
|
+
/** The question. */
|
|
376
|
+
message: string;
|
|
377
|
+
/** The text under it, if any. */
|
|
378
|
+
detail?: string;
|
|
379
|
+
/** The confirm button. */
|
|
380
|
+
confirm: string;
|
|
381
|
+
/** The cancel button (the player's "Cancel" by default). */
|
|
382
|
+
cancel: string;
|
|
383
|
+
/** The checkbox, if any. */
|
|
384
|
+
checkbox?: string;
|
|
385
|
+
}[];
|
|
264
386
|
/** The sequence provider's flags, `null` without a provider. */
|
|
265
387
|
sequence: {
|
|
266
388
|
/** The provider's `canNext`, as last evaluated. */
|
|
@@ -773,6 +895,27 @@ export declare class TestHost implements Transport {
|
|
|
773
895
|
/** Exact host names it resolves (a web resolver), with `schemes`. */
|
|
774
896
|
hosts?: string[];
|
|
775
897
|
}[];
|
|
898
|
+
/**
|
|
899
|
+
* What `plugin.resolvers.fileTypes` reports: the extensions of this
|
|
900
|
+
* plugin's file resolvers and of {@link TestHost.otherResolvers}, sorted,
|
|
901
|
+
* each once.
|
|
902
|
+
*/
|
|
903
|
+
fileTypes(): string[];
|
|
904
|
+
/**
|
|
905
|
+
* Push `resolvers.fileTypes` when {@link TestHost.fileTypes} changed:
|
|
906
|
+
* called after this plugin's resolvers come and go; call it yourself
|
|
907
|
+
* after changing {@link TestHost.otherResolvers} (another plugin started
|
|
908
|
+
* or stopped).
|
|
909
|
+
*
|
|
910
|
+
* @example
|
|
911
|
+
* ```ts
|
|
912
|
+
* host.otherResolvers.push({ owner: "vestra.nes", extensions: ["nes"] });
|
|
913
|
+
* host.updateFileTypes();
|
|
914
|
+
* await host.settle();
|
|
915
|
+
* assert.deepEqual(plugin.resolvers.fileTypes, ["nes"]);
|
|
916
|
+
* ```
|
|
917
|
+
*/
|
|
918
|
+
updateFileTypes(): void;
|
|
776
919
|
/**
|
|
777
920
|
* File opens waiting for this plugin's file resolver (`extensions`), by
|
|
778
921
|
* request id: the path and how it was opened. Opening a matching path
|
|
@@ -1095,7 +1238,7 @@ export declare class TestHost implements Transport {
|
|
|
1095
1238
|
/** Its version, `null` when unknown. */
|
|
1096
1239
|
version: string | null;
|
|
1097
1240
|
/** Where it was loaded from. */
|
|
1098
|
-
origin: "installed" | "development"
|
|
1241
|
+
origin: "installed" | "development";
|
|
1099
1242
|
/** Its run state. */
|
|
1100
1243
|
state: "starting" | "running" | "disabled" | "failed" | "stopped";
|
|
1101
1244
|
/** Why it failed (`state: "failed"`). */
|
|
@@ -1320,6 +1463,14 @@ export declare function startPlugin(activate: (plugin: Plugin) => unknown, optio
|
|
|
1320
1463
|
host?: TestHost;
|
|
1321
1464
|
/** Use an existing directory as `plugin.dataDirectory` (to test a restart). */
|
|
1322
1465
|
dataDirectory?: string;
|
|
1466
|
+
/**
|
|
1467
|
+
* The interface language to play ({@link TestHost.locale}): the plugin's
|
|
1468
|
+
* `locales/<locale>.po` (or `.json`) in `directory` is loaded and its
|
|
1469
|
+
* menus, messages and titles show translated. Default: the host's.
|
|
1470
|
+
*
|
|
1471
|
+
* @since 1.5.9
|
|
1472
|
+
*/
|
|
1473
|
+
locale?: Locale;
|
|
1323
1474
|
}): Promise<{
|
|
1324
1475
|
/** The test host the plugin talks to. */
|
|
1325
1476
|
host: TestHost;
|