@vestra-player/sdk 1.5.8 → 1.5.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +4 -1
- package/bin/vestra-plugin.mjs +170 -7
- package/dist/commands.d.ts +7 -4
- package/dist/i18n-tools.d.ts +67 -0
- package/dist/i18n-tools.js +787 -0
- package/dist/i18n.d.ts +222 -0
- package/dist/i18n.js +972 -0
- package/dist/player-runtime.d.ts +2 -0
- package/dist/player-runtime.js +2 -0
- package/dist/plugin/appearance.js +27 -1
- package/dist/plugin/bindings.js +1 -0
- package/dist/plugin/commands.js +24 -4
- package/dist/plugin/index.js +9 -1
- package/dist/plugin/language.d.ts +8 -0
- package/dist/plugin/language.js +79 -0
- package/dist/plugin/osd.d.ts +11 -0
- package/dist/plugin/osd.js +64 -9
- package/dist/plugin/packages.js +10 -0
- package/dist/plugin/runtime.d.ts +3 -1
- package/dist/plugin/runtime.js +6 -0
- package/dist/plugin/scope.d.ts +1 -0
- package/dist/plugin/scope.js +2 -0
- package/dist/plugin/subtitles.js +9 -1
- package/dist/plugin/testing.d.ts +184 -5
- package/dist/plugin/testing.js +459 -67
- package/dist/plugin/timeline.js +3 -1
- package/dist/plugin/types.d.ts +523 -54
- package/dist/plugin/ui.js +29 -10
- package/dist/plugin/values.d.ts +55 -0
- package/dist/plugin/values.js +117 -0
- package/dist/plugin/window.d.ts +2 -0
- package/dist/plugin/window.js +4 -0
- package/dist/source-resolvers.d.ts +0 -1
- package/dist/source-resolvers.js +9 -1
- package/dist/subtitles.d.ts +3 -1
- package/dist/wire.d.ts +38 -6
- package/dist/wire.js +13 -2
- package/llms-full.txt +646 -58
- package/llms.txt +44 -40
- package/package.json +1 -1
- package/slint/vestra/markdown.slint +5 -5
- package/slint/vestra/widgets.slint +4 -4
package/dist/player-runtime.d.ts
CHANGED
|
@@ -13,5 +13,7 @@ export { HostError } from "./wire.ts";
|
|
|
13
13
|
export type { Json, Transport } from "./wire.ts";
|
|
14
14
|
export { API_VERSION, CAPABILITIES } from "./capabilities.ts";
|
|
15
15
|
export { COMMAND_LIMITS, MENU_IDS } from "./commands.ts";
|
|
16
|
+
export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale, parsePo, rewriteSlint, renderText, scanSlint, TEXT_LIMITS, textKey, textProblem, } from "./i18n.ts";
|
|
16
17
|
export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.ts";
|
|
17
18
|
export { MARKDOWN_LIMITS } from "./plugin/markdown.ts";
|
|
19
|
+
export { UI_VALUE_LIMITS } from "./plugin/values.ts";
|
package/dist/player-runtime.js
CHANGED
|
@@ -11,5 +11,7 @@ export { createPlugin } from "./plugin/index.js";
|
|
|
11
11
|
export { HostError } from "./wire.js";
|
|
12
12
|
export { API_VERSION, CAPABILITIES } from "./capabilities.js";
|
|
13
13
|
export { COMMAND_LIMITS, MENU_IDS } from "./commands.js";
|
|
14
|
+
export { Catalog, HOST_ZH_HANS, LIST_SEPARATORS, LOCALES, MENU_KEYS, matchLocale, parsePo, rewriteSlint, renderText, scanSlint, TEXT_LIMITS, textKey, textProblem, } from "./i18n.js";
|
|
14
15
|
export { MANIFEST_FIELDS, MANIFEST_LIMITS } from "./manifest.js";
|
|
15
16
|
export { MARKDOWN_LIMITS } from "./plugin/markdown.js";
|
|
17
|
+
export { UI_VALUE_LIMITS } from "./plugin/values.js";
|
|
@@ -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/bindings.js
CHANGED
package/dist/plugin/commands.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { failure, Latest, quiet } from "./runtime.js";
|
|
3
3
|
import { checkBindings } from "./bindings.js";
|
|
4
4
|
import { MENU_IDS } from "../commands.js";
|
|
5
|
+
import { isTextValue, textProblem } from "../i18n.js";
|
|
5
6
|
const value = (item) => typeof item === "function" ? item() : item;
|
|
6
7
|
/** Validate options and build the wire declaration. */
|
|
7
8
|
function declare(options) {
|
|
@@ -11,7 +12,18 @@ function declare(options) {
|
|
|
11
12
|
throw failure("invalid", `command ${options.id}: title or path required`);
|
|
12
13
|
if (path.length > 8)
|
|
13
14
|
throw failure("invalid", `command ${options.id}: path has 1–8 segments`);
|
|
14
|
-
const
|
|
15
|
+
for (const segment of [title, ...path])
|
|
16
|
+
if (typeof segment !== "string") {
|
|
17
|
+
const problem = isTextValue(segment)
|
|
18
|
+
? "$vestraList" in segment
|
|
19
|
+
? "a menu text is a string or a text, not a list"
|
|
20
|
+
: textProblem(segment)
|
|
21
|
+
: "title and path segments are strings or texts";
|
|
22
|
+
if (problem)
|
|
23
|
+
throw failure("invalid", `command ${options.id}: ${problem}`);
|
|
24
|
+
}
|
|
25
|
+
const first = path[0];
|
|
26
|
+
const menu = typeof first === "string" && first.startsWith("@") ? first.slice(1) : null;
|
|
15
27
|
if (menu !== null && !MENU_IDS.includes(menu))
|
|
16
28
|
throw failure("invalid", `command ${options.id}: unknown menu id @${menu}; the built-in menus are ${MENU_IDS.map((id) => `@${id}`).join(", ")}`);
|
|
17
29
|
if (menu !== null && path.length < 2)
|
|
@@ -21,8 +33,8 @@ function declare(options) {
|
|
|
21
33
|
throw failure("invalid", `command ${options.id}: order must be an integer within ±1000000`);
|
|
22
34
|
const bindings = [...(options.bindings ?? [])];
|
|
23
35
|
return {
|
|
24
|
-
title,
|
|
25
|
-
path,
|
|
36
|
+
title: title,
|
|
37
|
+
path: path,
|
|
26
38
|
order,
|
|
27
39
|
group: options.group ?? "domain",
|
|
28
40
|
radio: options.radio ?? false,
|
|
@@ -89,11 +101,19 @@ class CommandHandle {
|
|
|
89
101
|
scope.add(this);
|
|
90
102
|
}
|
|
91
103
|
#state() {
|
|
104
|
+
const reason = value(this.#spec.reason);
|
|
105
|
+
if (reason !== null && typeof reason !== "string") {
|
|
106
|
+
const problem = isTextValue(reason)
|
|
107
|
+
? textProblem(reason)
|
|
108
|
+
: "reason is a string, a text or null";
|
|
109
|
+
if (problem)
|
|
110
|
+
throw failure("invalid", `command ${this.id}: ${problem}`);
|
|
111
|
+
}
|
|
92
112
|
return {
|
|
93
113
|
enabled: value(this.#spec.enabled),
|
|
94
114
|
checked: value(this.#spec.checked),
|
|
95
115
|
busy: value(this.#spec.busy),
|
|
96
|
-
reason
|
|
116
|
+
reason,
|
|
97
117
|
};
|
|
98
118
|
}
|
|
99
119
|
#sync() {
|
package/dist/plugin/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Runtime } from "./runtime.js";
|
|
1
|
+
import { failure, Runtime } from "./runtime.js";
|
|
2
2
|
import { ScopeImpl } from "./scope.js";
|
|
3
3
|
import { playerOf } from "./player.js";
|
|
4
4
|
import { windowOf } from "./window.js";
|
|
@@ -14,6 +14,14 @@ app) {
|
|
|
14
14
|
timelineOf(runtime).init(),
|
|
15
15
|
]);
|
|
16
16
|
const root = new ScopeImpl(runtime);
|
|
17
|
+
// Failures the plugin did not ask about: a property the player could not
|
|
18
|
+
// show when it rebuilt a panel or the Options window (an image removed
|
|
19
|
+
// since), a callback whose arguments it could not convert, a detached
|
|
20
|
+
// sidebar that could not be prepared. Older players send no code.
|
|
21
|
+
root.add(runtime.raw("ui.error", (data) => {
|
|
22
|
+
const { code, message } = (data ?? {});
|
|
23
|
+
root.report(failure(code ?? "ui", message ?? "user interface error"));
|
|
24
|
+
}));
|
|
17
25
|
return {
|
|
18
26
|
plugin: root,
|
|
19
27
|
activated: () => runtime.activated(),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ScopeImpl } from "./scope.ts";
|
|
2
|
+
import type { I18n, ListStyle, TemplateArgs, Text, TextArg, TextError, TextList } from "./types.ts";
|
|
3
|
+
/** `plugin.i18n.text`: a plain, frozen text value (no translation here). */
|
|
4
|
+
export declare function text(source: string, args?: TemplateArgs): Text;
|
|
5
|
+
/** `plugin.i18n.list`: a plain, frozen list value. */
|
|
6
|
+
export declare function list(items: readonly TextArg[], style?: ListStyle): TextList;
|
|
7
|
+
export declare const error: (source: string, args?: TemplateArgs) => TextError;
|
|
8
|
+
export declare const i18n: (scope: ScopeImpl) => I18n;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** `plugin.i18n`: the interface language of this run and text values (read-only). */
|
|
2
|
+
import { Catalog, isTextValue, LIST_STYLES, loadPackageCatalog, renderText, textKey, textProblem, } from "../i18n.js";
|
|
3
|
+
import { failure } from "./runtime.js";
|
|
4
|
+
/** Copy arguments (arrays and objects only; texts are already frozen). */
|
|
5
|
+
const copyArgs = (args) => Array.isArray(args) ? Object.freeze([...args]) : Object.freeze({ ...args });
|
|
6
|
+
/** `plugin.i18n.text`: a plain, frozen text value (no translation here). */
|
|
7
|
+
export function text(source, args) {
|
|
8
|
+
const value = { $vestraText: source };
|
|
9
|
+
if (args !== undefined && args !== null)
|
|
10
|
+
value.args = copyArgs(args);
|
|
11
|
+
const problem = textProblem(value);
|
|
12
|
+
if (problem)
|
|
13
|
+
throw failure("invalid", `text: ${problem}`);
|
|
14
|
+
return Object.freeze(value);
|
|
15
|
+
}
|
|
16
|
+
/** `plugin.i18n.list`: a plain, frozen list value. */
|
|
17
|
+
export function list(items, style) {
|
|
18
|
+
if (!Array.isArray(items))
|
|
19
|
+
throw failure("invalid", "list: items is an array");
|
|
20
|
+
if (style !== undefined &&
|
|
21
|
+
!LIST_STYLES.includes(style))
|
|
22
|
+
throw failure("invalid", `list: style is one of ${LIST_STYLES.join(", ")}`);
|
|
23
|
+
const value = {
|
|
24
|
+
$vestraList: Object.freeze([
|
|
25
|
+
...items,
|
|
26
|
+
]),
|
|
27
|
+
...(style === undefined ? {} : { style }),
|
|
28
|
+
};
|
|
29
|
+
const problem = textProblem(value);
|
|
30
|
+
if (problem)
|
|
31
|
+
throw failure("invalid", `list: ${problem}`);
|
|
32
|
+
return Object.freeze(value);
|
|
33
|
+
}
|
|
34
|
+
/** `plugin.i18n.error`: an Error whose message the player shows translated. */
|
|
35
|
+
class TextErrorImpl extends Error {
|
|
36
|
+
text;
|
|
37
|
+
constructor(value) {
|
|
38
|
+
super(textKey(value));
|
|
39
|
+
this.name = "TextError";
|
|
40
|
+
this.text = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export const error = (source, args) => new TextErrorImpl(text(source, args));
|
|
44
|
+
/** The plugin's catalog for this run's language, read once per runtime. */
|
|
45
|
+
const catalogs = new WeakMap();
|
|
46
|
+
function catalogOf(scope) {
|
|
47
|
+
const runtime = scope.runtime;
|
|
48
|
+
let catalog = catalogs.get(runtime);
|
|
49
|
+
if (!catalog) {
|
|
50
|
+
try {
|
|
51
|
+
catalog = loadPackageCatalog(runtime.directory, runtime.locale);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// As the player: a broken catalog is reported and treated as missing.
|
|
55
|
+
console.error(`[${scope.id}] i18n: ${error.message}`);
|
|
56
|
+
catalog = new Catalog();
|
|
57
|
+
}
|
|
58
|
+
catalogs.set(runtime, catalog);
|
|
59
|
+
}
|
|
60
|
+
return catalog;
|
|
61
|
+
}
|
|
62
|
+
export const i18n = (scope) => Object.freeze({
|
|
63
|
+
get locale() {
|
|
64
|
+
return scope.runtime.locale;
|
|
65
|
+
},
|
|
66
|
+
text,
|
|
67
|
+
list,
|
|
68
|
+
error,
|
|
69
|
+
resolve(value) {
|
|
70
|
+
if (typeof value === "string")
|
|
71
|
+
return catalogOf(scope).text(value);
|
|
72
|
+
const problem = isTextValue(value)
|
|
73
|
+
? textProblem(value)
|
|
74
|
+
: "resolve takes a string or a text value";
|
|
75
|
+
if (problem)
|
|
76
|
+
throw failure("invalid", `resolve: ${problem}`);
|
|
77
|
+
return renderText(value, catalogOf(scope), scope.runtime.locale);
|
|
78
|
+
},
|
|
79
|
+
});
|
package/dist/plugin/osd.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
import type { ScopeImpl } from "./scope.ts";
|
|
2
2
|
import type { Osd } from "./types.ts";
|
|
3
|
+
import type { TemplateArgsJson, TextValueJson } from "../wire.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Copy and check template arguments: an array or object of at most 16
|
|
6
|
+
* strings, finite numbers, texts and lists (`TEXT_LIMITS`).
|
|
7
|
+
*/
|
|
8
|
+
export declare function templateArgs(args: unknown): TemplateArgsJson | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Check a shown text (`osd.show`, dialogs): source text, or a text value
|
|
11
|
+
* that carries its own arguments (then `args` is not given).
|
|
12
|
+
*/
|
|
13
|
+
export declare function shownText(text: unknown, args: unknown, what?: string): TextValueJson;
|
|
3
14
|
export declare const osd: (scope: ScopeImpl) => Osd;
|
package/dist/plugin/osd.js
CHANGED
|
@@ -1,23 +1,78 @@
|
|
|
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
|
+
import { isTextValue, textProblem } from "../i18n.js";
|
|
5
|
+
/**
|
|
6
|
+
* Copy and check template arguments: an array or object of at most 16
|
|
7
|
+
* strings, finite numbers, texts and lists (`TEXT_LIMITS`).
|
|
8
|
+
*/
|
|
9
|
+
export function templateArgs(args) {
|
|
10
|
+
if (args === undefined || args === null)
|
|
11
|
+
return undefined;
|
|
12
|
+
const problem = textProblem(args, "args");
|
|
13
|
+
if (problem)
|
|
14
|
+
throw failure("invalid", problem);
|
|
15
|
+
return Array.isArray(args)
|
|
16
|
+
? [...args]
|
|
17
|
+
: { ...args };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check a shown text (`osd.show`, dialogs): source text, or a text value
|
|
21
|
+
* that carries its own arguments (then `args` is not given).
|
|
22
|
+
*/
|
|
23
|
+
export function shownText(text, args, what = "text") {
|
|
24
|
+
if (typeof text === "string")
|
|
25
|
+
return text;
|
|
26
|
+
if (isTextValue(text)) {
|
|
27
|
+
const problem = textProblem(text);
|
|
28
|
+
if (problem)
|
|
29
|
+
throw failure("invalid", `${what}: ${problem}`);
|
|
30
|
+
if (args !== undefined && args !== null)
|
|
31
|
+
throw failure("invalid", `${what} is a text value: its args go inside it, not in args`);
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
34
|
+
throw failure("invalid", `${what} is a string or a text value`);
|
|
35
|
+
}
|
|
3
36
|
class PanelHandle {
|
|
4
37
|
#scope;
|
|
5
38
|
#pump;
|
|
6
|
-
|
|
39
|
+
/** The last value sent, as JSON (`null` text: hidden). */
|
|
40
|
+
#last = JSON.stringify({ text: null });
|
|
7
41
|
#disposed = false;
|
|
8
42
|
constructor(scope) {
|
|
9
43
|
if (scope.runtime.services.has("osd.panel"))
|
|
10
44
|
throw new Error("a plugin can have only one osd panel");
|
|
11
45
|
scope.runtime.services.set("osd.panel", this);
|
|
12
46
|
this.#scope = scope;
|
|
13
|
-
this.#pump = new Latest((text) => scope.runtime.host.osd.panel(text));
|
|
47
|
+
this.#pump = new Latest(({ text, args, maxColumns }) => scope.runtime.host.osd.panel(text, args, maxColumns));
|
|
14
48
|
scope.add(this);
|
|
15
49
|
}
|
|
16
|
-
set(text) {
|
|
17
|
-
if (this.#disposed
|
|
50
|
+
set(text, args, options = {}) {
|
|
51
|
+
if (this.#disposed)
|
|
52
|
+
return;
|
|
53
|
+
let value;
|
|
54
|
+
try {
|
|
55
|
+
const max = options?.maxColumns;
|
|
56
|
+
if (max !== undefined &&
|
|
57
|
+
!(Number.isInteger(max) && max >= 8 && max <= 1000))
|
|
58
|
+
throw failure("invalid", "maxColumns is an integer from 8 to 1000");
|
|
59
|
+
value = {
|
|
60
|
+
text: text === null ? null : shownText(text, args),
|
|
61
|
+
...(text === null || args === undefined || args === null
|
|
62
|
+
? {}
|
|
63
|
+
: { args: templateArgs(args) }),
|
|
64
|
+
...(text === null || max === undefined ? {} : { maxColumns: max }),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
this.#scope.report(error);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const key = JSON.stringify(value);
|
|
72
|
+
if (key === this.#last)
|
|
18
73
|
return;
|
|
19
|
-
this.#last =
|
|
20
|
-
this.#pump.push(
|
|
74
|
+
this.#last = key;
|
|
75
|
+
this.#pump.push(value).catch((error) => this.#scope.report(error));
|
|
21
76
|
}
|
|
22
77
|
async dispose() {
|
|
23
78
|
if (this.#disposed)
|
|
@@ -25,12 +80,12 @@ class PanelHandle {
|
|
|
25
80
|
this.#disposed = true;
|
|
26
81
|
this.#scope.forget(this);
|
|
27
82
|
this.#scope.runtime.services.delete("osd.panel");
|
|
28
|
-
if (this.#last !== null)
|
|
29
|
-
await this.#pump.push(null).catch(() => { });
|
|
83
|
+
if (this.#last !== JSON.stringify({ text: null }))
|
|
84
|
+
await this.#pump.push({ text: null }).catch(() => { });
|
|
30
85
|
this.#pump.stop();
|
|
31
86
|
}
|
|
32
87
|
}
|
|
33
88
|
export const osd = (scope) => ({
|
|
34
|
-
show: (text) => scope.runtime.host.osd.show(text),
|
|
89
|
+
show: async (text, args) => scope.runtime.host.osd.show(shownText(text, args), templateArgs(args)),
|
|
35
90
|
panel: () => new PanelHandle(scope),
|
|
36
91
|
});
|
package/dist/plugin/packages.js
CHANGED
|
@@ -161,4 +161,14 @@ const info = (plugin) => Object.freeze({
|
|
|
161
161
|
...(warning.slot === undefined ? {} : { slot: warning.slot }),
|
|
162
162
|
...(warning.owner === undefined ? {} : { owner: warning.owner }),
|
|
163
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
|
+
: {}),
|
|
164
174
|
});
|
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/subtitles.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Subtitle selection, external SRT resources and this plugin's style layer. */
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { isTextValue, textProblem } from "../i18n.js";
|
|
3
4
|
import { Latest, failure, sleep, uncertain } from "./runtime.js";
|
|
4
5
|
import { abortError } from "./player.js";
|
|
5
6
|
const raws = new WeakMap();
|
|
@@ -226,7 +227,14 @@ export function subtitles(scope) {
|
|
|
226
227
|
if (select !== "primary" && select !== "secondary" && select !== "none")
|
|
227
228
|
throw failure("invalid", 'select must be "primary", "secondary" or "none"');
|
|
228
229
|
const title = options?.title;
|
|
229
|
-
|
|
230
|
+
// A text value (`plugin.i18n.text`) is checked here and its shown
|
|
231
|
+
// length by the player.
|
|
232
|
+
if (isTextValue(title)) {
|
|
233
|
+
const problem = textProblem(title);
|
|
234
|
+
if (problem)
|
|
235
|
+
throw failure("invalid", `title: ${problem}`);
|
|
236
|
+
}
|
|
237
|
+
else if (title !== undefined &&
|
|
230
238
|
(typeof title !== "string" ||
|
|
231
239
|
[...title].length < 1 ||
|
|
232
240
|
[...title].length > 200))
|