@vestra-player/sdk 1.5.11 → 1.5.13
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 +16 -0
- package/README.md +1 -1
- package/bin/vestra-plugin.mjs +4 -2
- package/dist/i18n-tools.js +85 -16
- package/dist/i18n.d.ts +9 -2
- package/dist/i18n.js +22 -6
- package/dist/plugin/testing.d.ts +15 -6
- package/dist/plugin/testing.js +35 -9
- package/llms-full.txt +4 -3
- package/llms.txt +12 -12
- package/package.json +1 -1
- package/slint/vestra/widgets.slint +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.13
|
|
4
|
+
|
|
5
|
+
**`i18n extract` keeps the translator's comments and writes the right number of plural forms.** A fix; `apiVersion` stays `vestra/1`.
|
|
6
|
+
|
|
7
|
+
- Merging into a `locales/<lang>.po` kept only the header's text: comment lines before the header (`# Reviewed by an AI reviewer on 2026-10-25`, `# Translator: …`, `#, fuzzy`) and the translator's own `# ` comments on entries were dropped. Both are now kept as written (`parsePo` returns `headerComments`, entries carry `comments`; `writePo` writes them).
|
|
8
|
+
- A plural entry has exactly `nplurals` `msgstr[n]` lines, from the file's `Plural-Forms` (or the language: one for `ja`, `ko`, `zh-Hans`). Before, every plural entry got an empty `msgstr[1]`, also in one-form languages; a merge now removes such an extra empty line (a filled one stays).
|
|
9
|
+
|
|
10
|
+
## 1.5.12
|
|
11
|
+
|
|
12
|
+
**Keys of disabled commands pass on; `i18n check` fails on stale catalogs; translator notes in tables; the Button follows its text.** A fix release; `apiVersion` stays `vestra/1`.
|
|
13
|
+
|
|
14
|
+
- **`TestHost.press` matches the player (0.4.3) for disabled commands.** A disabled command no longer swallows its key: the key goes to the next command bound to it (the one it shadowed in a conflict, then the global scope after a panel binding). With only disabled commands left, the first one with a `reason` shows the reason on the OSD and eats the key; without a reason the key reaches the focused control. `press` now resolves with what happened, `"ran"`, `"reason"` (see `host.osd`) or `"passed"`, instead of rejecting for a disabled command; it still rejects when nothing is bound and when the command that runs is busy. A test that expected `press` to reject for a disabled command now gets `"passed"` or `"reason"`.
|
|
15
|
+
- **`vestra-plugin i18n check` exits with 1** when `locales/messages.pot` is out of date, missing while `.po` files exist, or a `.po` lacks some of the template's messages, so CI catches a forgotten `extract`. Before, it printed these and exited 0. The other findings stay warnings, and `vestra-plugin check` still only prints them.
|
|
16
|
+
- **`TRANSLATORS:` notes on `as const` table entries.** A comment before a property or element of an object or array literal under `as const` (at any depth) now reaches the translators like one at a call site; before, texts extracted through a table lost their notes.
|
|
17
|
+
- **`slint/vestra/widgets.slint`** (the copy of the player's library): `Button`'s preferred width follows its label (the minimum width is unchanged), so a longer translated label widens the button instead of overflowing it.
|
|
18
|
+
|
|
3
19
|
## 1.5.11
|
|
4
20
|
|
|
5
21
|
**Text values: words mixed with data, translated by the player** (player 0.4.3). Additive; `apiVersion` stays `vestra/1`, and plugins that worked need no change. 1.5.9 and 1.5.10 were not published to npm; this release includes them. Round-2 i18n requests R2-audio-1, R2-video-1, R2-subtitles-1/2, R2-playlist-1/2, R2-store-1, R2-workshop-1, R2-nes-1, capture #1, P-2, the info and hotkeys export texts, timeline and subtitle titles: one mechanism. Guide: [Translate a plugin ▸ Text that mixes words and data](docs/how-to/i18n.md#text-that-mixes-words-and-data).
|
package/README.md
CHANGED
|
@@ -83,7 +83,7 @@ Complete, tested plugins in [`examples/`](https://github.com/vestrasys/vestra-pl
|
|
|
83
83
|
| `vestra-plugin build [dir...] [--version x.y.z]` | Writes `dist/`: types stripped, files copied, manifest versioned. |
|
|
84
84
|
| `vestra-plugin pack [dir...] [--version x.y.z] [--out dir]` | Builds, then packs `dist/` as `<id>-<version>.vxp`. |
|
|
85
85
|
| `vestra-plugin i18n extract [dir...] [--locale lang]` | Collects the texts to translate into `locales/`, keeping translations. |
|
|
86
|
-
| `vestra-plugin i18n check [dir...]` | Translation coverage and untranslatable text
|
|
86
|
+
| `vestra-plugin i18n check [dir...]` | Translation coverage and untranslatable text; fails on stale catalogs. |
|
|
87
87
|
|
|
88
88
|
## Compatibility
|
|
89
89
|
|
package/bin/vestra-plugin.mjs
CHANGED
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* i18n check coverage per language, placeholder and plural problems, a
|
|
30
30
|
* stale messages.pot, and texts that cannot be translated
|
|
31
31
|
* (composed in TypeScript, not literals, not English, Slint
|
|
32
|
-
* text without @tr)
|
|
32
|
+
* text without @tr) as warnings; fails (exit 1) on a stale
|
|
33
|
+
* messages.pot, a .po missing messages, an unreadable file
|
|
33
34
|
*
|
|
34
35
|
* `check` prints the same i18n warnings when the plugin has locales/;
|
|
35
36
|
* `build` compiles locales/<lang>.po to dist/locales/<lang>.json (the .po
|
|
@@ -430,7 +431,8 @@ async function i18nCheck(target) {
|
|
|
430
431
|
dependencies(target);
|
|
431
432
|
const { warnings, errors } = await i18nReport(target);
|
|
432
433
|
for (const line of warnings) console.log(`${target.name}: i18n: ${line}`);
|
|
433
|
-
|
|
434
|
+
// A stale messages.pot, a .po missing messages, an unreadable file.
|
|
435
|
+
if (errors.length) fail(`${target.name}: i18n: ${errors.join("; ")}`);
|
|
434
436
|
if (!warnings.length) console.log(`${target.name}: i18n: no problems`);
|
|
435
437
|
}
|
|
436
438
|
|
package/dist/i18n-tools.js
CHANGED
|
@@ -100,6 +100,34 @@ function ignored(node, text) {
|
|
|
100
100
|
.slice(node.getFullStart(), node.getStart())
|
|
101
101
|
.includes("i18n-ignore");
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* A string that is an entry (a property value or an element) of an object
|
|
105
|
+
* or array literal inside `… as const`, at any depth.
|
|
106
|
+
*/
|
|
107
|
+
function inConstTable(node) {
|
|
108
|
+
let current;
|
|
109
|
+
if (ts.isPropertyAssignment(node.parent) && node.parent.initializer === node)
|
|
110
|
+
current = node.parent.parent;
|
|
111
|
+
else if (ts.isArrayLiteralExpression(node.parent))
|
|
112
|
+
current = node.parent;
|
|
113
|
+
else
|
|
114
|
+
return false;
|
|
115
|
+
for (;;) {
|
|
116
|
+
const parent = current.parent;
|
|
117
|
+
if (ts.isAsExpression(parent) &&
|
|
118
|
+
ts.isTypeReferenceNode(parent.type) &&
|
|
119
|
+
ts.isIdentifier(parent.type.typeName) &&
|
|
120
|
+
parent.type.typeName.text === "const")
|
|
121
|
+
return true;
|
|
122
|
+
if (ts.isPropertyAssignment(parent))
|
|
123
|
+
current = parent.parent;
|
|
124
|
+
else if (ts.isArrayLiteralExpression(parent) ||
|
|
125
|
+
ts.isParenthesizedExpression(parent))
|
|
126
|
+
current = parent;
|
|
127
|
+
else
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
103
131
|
/** The statement (or class member) a node belongs to. */
|
|
104
132
|
function statementOf(node) {
|
|
105
133
|
let current = node;
|
|
@@ -111,6 +139,11 @@ function statementOf(node) {
|
|
|
111
139
|
}
|
|
112
140
|
class Extractor {
|
|
113
141
|
found = [];
|
|
142
|
+
/**
|
|
143
|
+
* `TRANSLATORS:` notes on entries of `as const` tables, by text: a table
|
|
144
|
+
* entry is extracted through its literal type, which has no comments.
|
|
145
|
+
*/
|
|
146
|
+
tableNotes = new Map();
|
|
114
147
|
findings = [];
|
|
115
148
|
#checker;
|
|
116
149
|
constructor(checker) {
|
|
@@ -302,6 +335,20 @@ class Extractor {
|
|
|
302
335
|
const visit = (node) => {
|
|
303
336
|
if (ts.isCallExpression(node))
|
|
304
337
|
this.#call(node, file, source, text);
|
|
338
|
+
if ((ts.isStringLiteral(node) ||
|
|
339
|
+
ts.isNoSubstitutionTemplateLiteral(node)) &&
|
|
340
|
+
inConstTable(node)) {
|
|
341
|
+
// The comment sits before the entry: the property, or the element.
|
|
342
|
+
const entry = ts.isPropertyAssignment(node.parent) ? node.parent : node;
|
|
343
|
+
const notes = notesOf(entry, text);
|
|
344
|
+
if (notes.length) {
|
|
345
|
+
const known = this.tableNotes.get(node.text) ?? [];
|
|
346
|
+
for (const note of notes)
|
|
347
|
+
if (!known.includes(note))
|
|
348
|
+
known.push(note);
|
|
349
|
+
this.tableNotes.set(node.text, known);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
305
352
|
// A text value written as data: `{ $vestraText: "…" }`.
|
|
306
353
|
if (ts.isPropertyAssignment(node) &&
|
|
307
354
|
(ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) &&
|
|
@@ -589,7 +636,10 @@ export function extract(options) {
|
|
|
589
636
|
}
|
|
590
637
|
if (occurrence.plural !== undefined && message.plural === undefined)
|
|
591
638
|
message.plural = occurrence.plural;
|
|
592
|
-
|
|
639
|
+
const tableNotes = occurrence.context === undefined
|
|
640
|
+
? (extractor.tableNotes.get(occurrence.id) ?? [])
|
|
641
|
+
: [];
|
|
642
|
+
for (const note of [...occurrence.notes, ...tableNotes])
|
|
593
643
|
if (!message.notes.includes(note))
|
|
594
644
|
message.notes.push(note);
|
|
595
645
|
if (!message.references.includes(reference))
|
|
@@ -598,10 +648,17 @@ export function extract(options) {
|
|
|
598
648
|
const findings = extractor.findings.sort((a, b) => a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1);
|
|
599
649
|
return { messages: [...messages.values()], findings };
|
|
600
650
|
}
|
|
601
|
-
|
|
651
|
+
/** `msgstr[n]` lines a plural entry has: the header's `nplurals`, else the language's. */
|
|
652
|
+
const formsOf = (language, header) => {
|
|
653
|
+
const declared = /nplurals\s*=\s*(\d+)/.exec(header ?? "")?.[1];
|
|
654
|
+
if (declared !== undefined && Number(declared) >= 1)
|
|
655
|
+
return Number(declared);
|
|
656
|
+
return ONE_FORM.has(language) ? 1 : 2;
|
|
657
|
+
};
|
|
658
|
+
const entryOf = (message, forms = 2) => {
|
|
602
659
|
const entry = {
|
|
603
660
|
id: message.id,
|
|
604
|
-
strings: message.plural === undefined ? [""] :
|
|
661
|
+
strings: message.plural === undefined ? [""] : Array(forms).fill(""),
|
|
605
662
|
fuzzy: false,
|
|
606
663
|
notes: [...message.notes],
|
|
607
664
|
references: [...message.references],
|
|
@@ -619,7 +676,7 @@ export function template(manifest, messages) {
|
|
|
619
676
|
const project = [manifest.id, manifest.version]
|
|
620
677
|
.filter((part) => typeof part === "string")
|
|
621
678
|
.join(" ");
|
|
622
|
-
return writePo(`Project-Id-Version: ${project}\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n`, messages.map(entryOf));
|
|
679
|
+
return writePo(`Project-Id-Version: ${project}\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n`, messages.map((message) => entryOf(message)));
|
|
623
680
|
}
|
|
624
681
|
/** The header of a new `<lang>.po`. */
|
|
625
682
|
export const newHeader = (language) => `Language: ${language}\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nPlural-Forms: ${pluralForms(language)}\n`;
|
|
@@ -637,20 +694,30 @@ export function merge(existing, language, messages) {
|
|
|
637
694
|
old.set(keyOf(entry.context, entry.id), entry);
|
|
638
695
|
const result = [];
|
|
639
696
|
const present = new Set();
|
|
697
|
+
// A plural entry has exactly nplurals msgstr[n] (one for ja, ko,
|
|
698
|
+
// zh-Hans); extra empty ones are dropped, filled ones kept.
|
|
699
|
+
const forms = formsOf(language, parsed?.header ?? newHeader(language));
|
|
640
700
|
let added = 0;
|
|
641
701
|
let translated = 0;
|
|
642
702
|
for (const message of messages) {
|
|
643
703
|
const key = keyOf(message.context, message.id);
|
|
644
704
|
present.add(key);
|
|
645
|
-
const entry = entryOf(message);
|
|
705
|
+
const entry = entryOf(message, forms);
|
|
646
706
|
const before = old.get(key);
|
|
707
|
+
// The translator's own comments stay with their message.
|
|
708
|
+
if (before?.comments?.length)
|
|
709
|
+
entry.comments = [...before.comments];
|
|
647
710
|
if (before && hasText(before)) {
|
|
648
711
|
entry.strings = [...before.strings];
|
|
649
712
|
if (message.plural === undefined)
|
|
650
713
|
entry.strings = [entry.strings[0]];
|
|
651
|
-
else
|
|
652
|
-
while (entry.strings.length <
|
|
714
|
+
else {
|
|
715
|
+
while (entry.strings.length < forms)
|
|
653
716
|
entry.strings.push("");
|
|
717
|
+
while (entry.strings.length > forms &&
|
|
718
|
+
entry.strings[entry.strings.length - 1] === "")
|
|
719
|
+
entry.strings.pop();
|
|
720
|
+
}
|
|
654
721
|
entry.fuzzy = before.fuzzy;
|
|
655
722
|
entry.flags = [...before.flags];
|
|
656
723
|
}
|
|
@@ -668,7 +735,7 @@ export function merge(existing, language, messages) {
|
|
|
668
735
|
obsolete++;
|
|
669
736
|
}
|
|
670
737
|
return {
|
|
671
|
-
text: writePo(parsed?.header ?? newHeader(language), result),
|
|
738
|
+
text: writePo(parsed?.header ?? newHeader(language), result, parsed?.headerComments ?? []),
|
|
672
739
|
total: messages.length,
|
|
673
740
|
translated,
|
|
674
741
|
added,
|
|
@@ -683,24 +750,26 @@ export function checkCatalogs(directory, messages, findings) {
|
|
|
683
750
|
const pot = join(locales, "messages.pot");
|
|
684
751
|
const keys = (entries) => new Set(entries.map((e) => `${keyOf(e.context, e.id)}\u0000${e.plural ?? ""}`));
|
|
685
752
|
const wanted = keys(messages);
|
|
753
|
+
const files = existsSync(locales)
|
|
754
|
+
? readdirSync(locales)
|
|
755
|
+
.filter((name) => name.endsWith(".po"))
|
|
756
|
+
.sort()
|
|
757
|
+
: [];
|
|
758
|
+
// A stale template or a .po without some of its messages fails
|
|
759
|
+
// `i18n check` (an error); `check` prints it like the warnings.
|
|
686
760
|
if (!existsSync(pot))
|
|
687
|
-
warnings.push("locales/messages.pot is missing: run vestra-plugin i18n extract");
|
|
761
|
+
(files.length ? errors : warnings).push("locales/messages.pot is missing: run vestra-plugin i18n extract");
|
|
688
762
|
else {
|
|
689
763
|
try {
|
|
690
764
|
const have = keys(parsePo(readFileSync(pot, "utf8")).entries);
|
|
691
765
|
const same = have.size === wanted.size && [...wanted].every((key) => have.has(key));
|
|
692
766
|
if (!same)
|
|
693
|
-
|
|
767
|
+
errors.push("locales/messages.pot is out of date: run vestra-plugin i18n extract");
|
|
694
768
|
}
|
|
695
769
|
catch (error) {
|
|
696
770
|
errors.push(`locales/messages.pot: ${error.message}`);
|
|
697
771
|
}
|
|
698
772
|
}
|
|
699
|
-
const files = existsSync(locales)
|
|
700
|
-
? readdirSync(locales)
|
|
701
|
-
.filter((name) => name.endsWith(".po"))
|
|
702
|
-
.sort()
|
|
703
|
-
: [];
|
|
704
773
|
for (const name of files) {
|
|
705
774
|
const language = name.slice(0, -3);
|
|
706
775
|
if (language === "en") {
|
|
@@ -729,7 +798,7 @@ export function checkCatalogs(directory, messages, findings) {
|
|
|
729
798
|
const inFile = keys(live);
|
|
730
799
|
const missing = [...wanted].filter((key) => !inFile.has(key)).length;
|
|
731
800
|
if (missing)
|
|
732
|
-
|
|
801
|
+
errors.push(`locales/${name}: ${missing} ${missing === 1 ? "message is" : "messages are"} not in this file: run vestra-plugin i18n extract`);
|
|
733
802
|
for (const entry of live) {
|
|
734
803
|
entry.strings.forEach((text, form) => {
|
|
735
804
|
if (text === "")
|
package/dist/i18n.d.ts
CHANGED
|
@@ -25,16 +25,23 @@ export interface PoEntry {
|
|
|
25
25
|
flags: string[];
|
|
26
26
|
/** A `#~` entry: no longer in the source. */
|
|
27
27
|
obsolete: boolean;
|
|
28
|
+
/** `# ` lines: the translator's own comments, kept as written. */
|
|
29
|
+
comments?: string[];
|
|
28
30
|
}
|
|
29
31
|
/** The catalog key: `msgid`, or `msgctxt` U+0004 `msgid`. */
|
|
30
32
|
export declare const keyOf: (context: string | undefined, id: string) => string;
|
|
31
33
|
/** Parse a `.po` (or `.pot`) file. Throws with the line number on errors. */
|
|
32
34
|
export declare function parsePo(text: string): {
|
|
33
35
|
header: string;
|
|
36
|
+
/** The comment lines before the header entry, as written (`# Reviewed…`, `#, fuzzy`). */
|
|
37
|
+
headerComments: string[];
|
|
34
38
|
entries: PoEntry[];
|
|
35
39
|
};
|
|
36
|
-
/**
|
|
37
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Write a `.po` file: the header (after its comment lines, as written),
|
|
42
|
+
* then the entries in the given order.
|
|
43
|
+
*/
|
|
44
|
+
export declare function writePo(header: string, entries: readonly PoEntry[], headerComments?: readonly string[]): string;
|
|
38
45
|
/** A message of a compiled catalog: a translation, or `[one, other]`. */
|
|
39
46
|
export type CompiledMessage = string | [string, string];
|
|
40
47
|
/**
|
package/dist/i18n.js
CHANGED
|
@@ -23,6 +23,9 @@ const translated = (entry) => !entry.fuzzy && !entry.obsolete && (entry.strings[
|
|
|
23
23
|
export function parsePo(text) {
|
|
24
24
|
const entries = [];
|
|
25
25
|
let header = "";
|
|
26
|
+
let headerComments = [];
|
|
27
|
+
// Every comment line of the entry being read, as written.
|
|
28
|
+
let raw = [];
|
|
26
29
|
const blank = () => ({
|
|
27
30
|
id: "",
|
|
28
31
|
strings: [],
|
|
@@ -39,11 +42,14 @@ export function parsePo(text) {
|
|
|
39
42
|
let pending = blank();
|
|
40
43
|
const finish = () => {
|
|
41
44
|
if (started) {
|
|
42
|
-
if (entry.id === "" && entry.context === undefined)
|
|
45
|
+
if (entry.id === "" && entry.context === undefined) {
|
|
43
46
|
header = entry.strings[0] ?? "";
|
|
47
|
+
headerComments = raw;
|
|
48
|
+
}
|
|
44
49
|
else
|
|
45
50
|
entries.push(entry);
|
|
46
51
|
}
|
|
52
|
+
raw = [];
|
|
47
53
|
entry = blank();
|
|
48
54
|
started = false;
|
|
49
55
|
field = "none";
|
|
@@ -66,8 +72,11 @@ export function parsePo(text) {
|
|
|
66
72
|
else if (line.startsWith("#")) {
|
|
67
73
|
if (started)
|
|
68
74
|
finish();
|
|
75
|
+
raw.push(lines[index].trimEnd());
|
|
69
76
|
const body = line.slice(2).trim();
|
|
70
|
-
if (line.startsWith("
|
|
77
|
+
if (line === "#" || line.startsWith("# "))
|
|
78
|
+
(pending.comments ??= []).push(line.slice(2));
|
|
79
|
+
else if (line.startsWith("#."))
|
|
71
80
|
pending.notes.push(body);
|
|
72
81
|
else if (line.startsWith("#:"))
|
|
73
82
|
pending.references.push(...body.split(/\s+/).filter(Boolean));
|
|
@@ -135,7 +144,7 @@ export function parsePo(text) {
|
|
|
135
144
|
field = next;
|
|
136
145
|
}
|
|
137
146
|
finish();
|
|
138
|
-
return { header, entries };
|
|
147
|
+
return { header, headerComments, entries };
|
|
139
148
|
}
|
|
140
149
|
function unquote(text, at) {
|
|
141
150
|
if (text.length < 2 || !text.startsWith('"') || !text.endsWith('"'))
|
|
@@ -172,12 +181,19 @@ function field(keyword, text) {
|
|
|
172
181
|
return `${keyword} ${poString(text)}`;
|
|
173
182
|
return [`${keyword} ""`, ...parts.map(poString)].join("\n");
|
|
174
183
|
}
|
|
175
|
-
/**
|
|
176
|
-
|
|
177
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Write a `.po` file: the header (after its comment lines, as written),
|
|
186
|
+
* then the entries in the given order.
|
|
187
|
+
*/
|
|
188
|
+
export function writePo(header, entries, headerComments = []) {
|
|
189
|
+
const blocks = [
|
|
190
|
+
[...headerComments, `msgid ""`, field("msgstr", header)].join("\n"),
|
|
191
|
+
];
|
|
178
192
|
for (const entry of entries) {
|
|
179
193
|
const prefix = entry.obsolete ? "#~ " : "";
|
|
180
194
|
const lines = [];
|
|
195
|
+
for (const comment of entry.comments ?? [])
|
|
196
|
+
lines.push(comment === "" ? "#" : `# ${comment}`);
|
|
181
197
|
for (const note of entry.notes)
|
|
182
198
|
lines.push(`#. ${note}`);
|
|
183
199
|
if (entry.references.length && !entry.obsolete)
|
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -1242,11 +1242,20 @@ export declare class TestHost implements Transport {
|
|
|
1242
1242
|
failStream(producerId: number, message: string): void;
|
|
1243
1243
|
/**
|
|
1244
1244
|
* Press a binding like the host's table would: panel-focus bindings first
|
|
1245
|
-
* when `panel` is true, then global ones;
|
|
1246
|
-
*
|
|
1247
|
-
*
|
|
1248
|
-
* (
|
|
1249
|
-
*
|
|
1245
|
+
* when `panel` is true, then global ones; within a scope the earliest
|
|
1246
|
+
* registration first (the player's own commands before the plugin's), as
|
|
1247
|
+
* the conflict rule orders them. A disabled command lets the key pass to
|
|
1248
|
+
* the next command bound to it (SDK 1.5.12, as player 0.4.3): the one it
|
|
1249
|
+
* shadowed, then the global scope. The first enabled one runs; `press`
|
|
1250
|
+
* resolves after it ran (`"ran"`), or rejects when it is busy. When only
|
|
1251
|
+
* disabled commands hold the key, the first one with a `reason` shows the
|
|
1252
|
+
* reason on the OSD and eats the key (`"reason"`, see `host.osd`);
|
|
1253
|
+
* without a reason the key goes on to the focused control (`"passed"`,
|
|
1254
|
+
* nothing runs). Rejects when nothing is bound. Bindings use the
|
|
1255
|
+
* reference syntax (`"Space"`, `"Ctrl+O"`, `"Mouse:WheelUp"`), and user
|
|
1256
|
+
* overrides in `bindings.overlay` apply.
|
|
1257
|
+
*
|
|
1258
|
+
* @returns What the key did: `"ran"`, `"reason"` or `"passed"`.
|
|
1250
1259
|
*
|
|
1251
1260
|
* @example
|
|
1252
1261
|
* ```ts
|
|
@@ -1256,7 +1265,7 @@ export declare class TestHost implements Transport {
|
|
|
1256
1265
|
*/
|
|
1257
1266
|
press(binding: string, options?: {
|
|
1258
1267
|
panel?: boolean;
|
|
1259
|
-
}): Promise<
|
|
1268
|
+
}): Promise<"ran" | "reason" | "passed">;
|
|
1260
1269
|
/**
|
|
1261
1270
|
* Bindings declared by more than one command, each as
|
|
1262
1271
|
* `[binding, winner, ...ignored]` (command ids; core actions as
|
package/dist/plugin/testing.js
CHANGED
|
@@ -1749,11 +1749,20 @@ export class TestHost {
|
|
|
1749
1749
|
}
|
|
1750
1750
|
/**
|
|
1751
1751
|
* Press a binding like the host's table would: panel-focus bindings first
|
|
1752
|
-
* when `panel` is true, then global ones;
|
|
1753
|
-
*
|
|
1754
|
-
*
|
|
1755
|
-
* (
|
|
1756
|
-
*
|
|
1752
|
+
* when `panel` is true, then global ones; within a scope the earliest
|
|
1753
|
+
* registration first (the player's own commands before the plugin's), as
|
|
1754
|
+
* the conflict rule orders them. A disabled command lets the key pass to
|
|
1755
|
+
* the next command bound to it (SDK 1.5.12, as player 0.4.3): the one it
|
|
1756
|
+
* shadowed, then the global scope. The first enabled one runs; `press`
|
|
1757
|
+
* resolves after it ran (`"ran"`), or rejects when it is busy. When only
|
|
1758
|
+
* disabled commands hold the key, the first one with a `reason` shows the
|
|
1759
|
+
* reason on the OSD and eats the key (`"reason"`, see `host.osd`);
|
|
1760
|
+
* without a reason the key goes on to the focused control (`"passed"`,
|
|
1761
|
+
* nothing runs). Rejects when nothing is bound. Bindings use the
|
|
1762
|
+
* reference syntax (`"Space"`, `"Ctrl+O"`, `"Mouse:WheelUp"`), and user
|
|
1763
|
+
* overrides in `bindings.overlay` apply.
|
|
1764
|
+
*
|
|
1765
|
+
* @returns What the key did: `"ran"`, `"reason"` or `"passed"`.
|
|
1757
1766
|
*
|
|
1758
1767
|
* @example
|
|
1759
1768
|
* ```ts
|
|
@@ -1763,11 +1772,28 @@ export class TestHost {
|
|
|
1763
1772
|
*/
|
|
1764
1773
|
async press(binding, options = {}) {
|
|
1765
1774
|
const claims = this.#claims();
|
|
1766
|
-
const
|
|
1767
|
-
claims.get(`
|
|
1768
|
-
|
|
1775
|
+
const ids = [
|
|
1776
|
+
...(options.panel ? (claims.get(`panel ${binding}`) ?? []) : []),
|
|
1777
|
+
...(claims.get(`global ${binding}`) ?? []),
|
|
1778
|
+
];
|
|
1779
|
+
if (!ids.length)
|
|
1769
1780
|
throw new Error(`test host: nothing is bound to ${binding}`);
|
|
1770
|
-
|
|
1781
|
+
const command = (id) => this.coreCommands.get(id) ?? this.command(id);
|
|
1782
|
+
const enabled = ids.find((id) => command(id).state.enabled);
|
|
1783
|
+
if (enabled !== undefined) {
|
|
1784
|
+
await command(enabled).run();
|
|
1785
|
+
return "ran";
|
|
1786
|
+
}
|
|
1787
|
+
for (const id of ids) {
|
|
1788
|
+
const reason = this.commands.has(id)
|
|
1789
|
+
? command(id).display.reason
|
|
1790
|
+
: command(id).state.reason;
|
|
1791
|
+
if (typeof reason === "string" && reason !== "") {
|
|
1792
|
+
this.osd.push(reason);
|
|
1793
|
+
return "reason";
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return "passed";
|
|
1771
1797
|
}
|
|
1772
1798
|
/**
|
|
1773
1799
|
* Bindings declared by more than one command, each as
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.13: full reference
|
|
2
2
|
|
|
3
3
|
> Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
|
|
4
4
|
|
|
@@ -5230,11 +5230,12 @@ class TestHost implements Transport {
|
|
|
5230
5230
|
*/
|
|
5231
5231
|
failStream(producerId: number, message: string): void;
|
|
5232
5232
|
/**
|
|
5233
|
-
* Press a binding like the host's table would: panel-focus bindings first when `panel` is true, then global ones; the earliest registration
|
|
5233
|
+
* Press a binding like the host's table would: panel-focus bindings first when `panel` is true, then global ones; within a scope the earliest registration first (the player's own commands before the plugin's), as the conflict rule orders them. A disabled command lets the key pass to the next command bound to it (SDK 1.5.12, as player 0.4.3): the one it shadowed, then the global scope. The first enabled one runs; `press` resolves after it ran (`"ran"`), or rejects when it is busy. When only disabled commands hold the key, the first one with a `reason` shows the reason on the OSD and eats the key (`"reason"`, see `host.osd`); without a reason the key goes on to the focused control (`"passed"`, nothing runs). Rejects when nothing is bound. Bindings use the reference syntax (`"Space"`, `"Ctrl+O"`, `"Mouse:WheelUp"`), and user overrides in `bindings.overlay` apply.
|
|
5234
|
+
* @returns What the key did: `"ran"`, `"reason"` or `"passed"`.
|
|
5234
5235
|
*/
|
|
5235
5236
|
press(binding: string, options?: {
|
|
5236
5237
|
panel?: boolean;
|
|
5237
|
-
}): Promise<
|
|
5238
|
+
}): Promise<"ran" | "reason" | "passed">;
|
|
5238
5239
|
/** Bindings declared by more than one command, each as `[binding, winner, ...ignored]` (command ids; core actions as `host/<id>`). Empty when there are none. */
|
|
5239
5240
|
get conflicts(): string[][];
|
|
5240
5241
|
/** The binding overlay `plugin.bindings.set` writes (`"<plugin>/<command>"` to inputs; preset it to simulate saved overrides). `press` honours it. */
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vestra Player SDK
|
|
2
2
|
|
|
3
|
-
> @vestra-player/sdk 1.5.
|
|
3
|
+
> @vestra-player/sdk 1.5.13: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process. llms-full.txt, next to this file, has every declaration, the guides and the examples; L<n> below are its line numbers.
|
|
4
4
|
|
|
5
5
|
**How plugins run**
|
|
6
6
|
|
|
@@ -121,7 +121,7 @@ test("shows the position", async () => {
|
|
|
121
121
|
- `vestra-plugin build [dir...] [--version x.y.z]`: Writes `dist/`: types stripped, files copied, manifest versioned.
|
|
122
122
|
- `vestra-plugin pack [dir...] [--version x.y.z] [--out dir]`: Builds, then packs `dist/` as `<id>-<version>.vxp`.
|
|
123
123
|
- `vestra-plugin i18n extract [dir...] [--locale lang]`: Collects the texts to translate into `locales/`, keeping translations.
|
|
124
|
-
- `vestra-plugin i18n check [dir...]`: Translation coverage and untranslatable text
|
|
124
|
+
- `vestra-plugin i18n check [dir...]`: Translation coverage and untranslatable text; fails on stale catalogs.
|
|
125
125
|
- `vestra-player --plugin <dir>` runs a plugin from source, no build needed.
|
|
126
126
|
|
|
127
127
|
**Error codes** (`error.code`; branch on it, never on `message`)
|
|
@@ -153,7 +153,7 @@ test("shows the position", async () => {
|
|
|
153
153
|
- [How-to: Keep a slider responsive while dragging](https://vestrasys.github.io/vestra-player-sdk/documents/Keep_a_slider_responsive_while_dragging.html): L627–801
|
|
154
154
|
- [How-to: Remember a window's state between runs](https://vestrasys.github.io/vestra-player-sdk/documents/Remember_a_window_s_state_between_runs.html): L802–931
|
|
155
155
|
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L932–4427
|
|
156
|
-
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html) (TestHost, startPlugin): L4428–
|
|
156
|
+
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html) (TestHost, startPlugin): L4428–5538
|
|
157
157
|
|
|
158
158
|
## API
|
|
159
159
|
|
|
@@ -193,15 +193,15 @@ Where each module of `plugin` is declared:
|
|
|
193
193
|
|
|
194
194
|
Complete plugins with tests, in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples) and in llms-full.txt:
|
|
195
195
|
|
|
196
|
-
- hello: The README Quick start: one menu command and an on-screen message.
|
|
197
|
-
- blur: A tool window with one slider that blurs the playing video.
|
|
198
|
-
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone.
|
|
199
|
-
- bookmark: A tool window bound to the playback state: remember the position and jump back to it.
|
|
200
|
-
- panel-menu: A sidebar panel with its own right-click menu.
|
|
201
|
-
- menus: Radio menu items, separator groups and a menu item whose title changes.
|
|
202
|
-
- theme-widgets: A tool window drawn with the player's theme and widgets.
|
|
203
|
-
- markdown: A window that shows a Markdown document.
|
|
204
|
-
- i18n: A plugin in the player's language: English source text, a Chinese translation.
|
|
196
|
+
- hello: The README Quick start: one menu command and an on-screen message. L5955–6044
|
|
197
|
+
- blur: A tool window with one slider that blurs the playing video. L6045–6306
|
|
198
|
+
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L6307–6639
|
|
199
|
+
- bookmark: A tool window bound to the playback state: remember the position and jump back to it. L6640–6824
|
|
200
|
+
- panel-menu: A sidebar panel with its own right-click menu. L6825–7030
|
|
201
|
+
- menus: Radio menu items, separator groups and a menu item whose title changes. L7031–7190
|
|
202
|
+
- theme-widgets: A tool window drawn with the player's theme and widgets. L7191–7354
|
|
203
|
+
- markdown: A window that shows a Markdown document. L7355–7548
|
|
204
|
+
- i18n: A plugin in the player's language: English source text, a Chinese translation. L7549–7716
|
|
205
205
|
|
|
206
206
|
## Optional
|
|
207
207
|
|
package/package.json
CHANGED
|
@@ -81,13 +81,17 @@ export component Button {
|
|
|
81
81
|
min-height: root.icon-only ? root.icon-height : root.small ? Theme.button-small-height : Theme.button-height;
|
|
82
82
|
min-width: root.icon-only ? root.icon-width : root.small ? Theme.button-small-min-width : Theme.button-min-width;
|
|
83
83
|
preferred-height: self.min-height;
|
|
84
|
+
// A longer (translated) label widens the button instead of overflowing it.
|
|
85
|
+
preferred-width: root.icon-only ? self.min-width
|
|
86
|
+
: max(self.min-width, label.preferred-width + 2 * Theme.button-padding);
|
|
84
87
|
accessible-role: button;
|
|
85
88
|
accessible-label: root.text;
|
|
86
89
|
accessible-enabled: root.enabled;
|
|
87
90
|
accessible-checkable: root.icon-only;
|
|
88
91
|
accessible-checked: root.checked;
|
|
89
92
|
accessible-action-default => { if root.enabled { root.clicked(); } }
|
|
90
|
-
|
|
93
|
+
Rectangle {
|
|
94
|
+
visible: !root.icon-only;
|
|
91
95
|
border-radius: Theme.radius-control;
|
|
92
96
|
background: !root.enabled ? Theme.button-disabled
|
|
93
97
|
: touch.pressed ? Theme.button-press
|
|
@@ -96,7 +100,7 @@ export component Button {
|
|
|
96
100
|
padding-left: Theme.button-padding;
|
|
97
101
|
padding-right: Theme.button-padding;
|
|
98
102
|
alignment: center;
|
|
99
|
-
Text {
|
|
103
|
+
label := Text {
|
|
100
104
|
text: root.text;
|
|
101
105
|
color: root.enabled ? Theme.text-list : Theme.text-dim;
|
|
102
106
|
font-size: Theme.font-body;
|