@vestra-player/sdk 1.5.45 → 1.5.46
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 +15 -0
- package/bin/vestra-plugin.mjs +59 -1
- package/dist/plugin/patch.d.ts +52 -1
- package/dist/plugin/patch.js +104 -6
- package/dist/plugin/testing.js +60 -3
- package/dist/plugin/types.d.ts +17 -14
- package/dist/plugin/ui.d.ts +8 -2
- package/dist/plugin/ui.js +165 -11
- package/dist/plugin/values.d.ts +21 -36
- package/dist/plugin/values.js +22 -37
- package/dist/wire.d.ts +3 -0
- package/llms-full.txt +46 -53
- package/llms.txt +22 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.46
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. The new limits and the split writes take effect with player 0.6.0 (large lists P3); with older players arrays are sent whole as before and the old limits apply there. **A plugin that binds a whole long list depends on them: set `"minPlayerVersion": "0.6.0"`.**
|
|
6
|
+
|
|
7
|
+
- **Whole lists.** `UI_VALUE_LIMITS` follow player 0.6.0: 100,000 rows per array (was 4,096), 1,048,576 values (was 16,384) and 32 MiB of text (was 1 MiB) in one property value. A plugin binds its whole list; a long list should be drawn with a Slint `ListView` (a `for` builds every row). The windowing example is replaced by a whole-list one.
|
|
8
|
+
- **Writes larger than one request are split.** The protocol line stays 1 MiB (`MESSAGE_LIMIT`). With a player that takes row patches:
|
|
9
|
+
- an array whose rows exceed 512 KiB (`CHUNK_BYTES`, internal) is sent as a whole write of its first rows and `insert` patches of the rest, each applying on the one before; every part but the last carries `more: true`, and the player holds them and writes the whole value at the last, so a list it already shows changes once (its scroll position and selection stay);
|
|
10
|
+
- a patch carrying more is sent as several patches, each applying on the one before; a diff may now plan up to 16,384 operations (at most 1,024 moves), sent 1,024 at most per patch, before the value goes whole;
|
|
11
|
+
- the initial properties of `panels.mount`, `ui.open` and `settings.page` carry at most 512 KiB of rows together: the first rows show at once and the rest follows as patches on the first update;
|
|
12
|
+
- only a property's top-level array is split, and not one whose rows hold Markdown documents (sent whole as before: the player merges documents in place); any other value, and each row, must fit in one request. A row of more than 512 KiB, or a value over the limits, is refused with `limit` before any part is sent;
|
|
13
|
+
- when the view closes or shows its initial properties again while a write goes in parts, the rest is dropped without an error and the view's next update sends what it shows; any other failure part-way leaves no base, so the next write of the property is whole (in parts again).
|
|
14
|
+
- **`vestra-plugin check` warns** (a reminder, never a failure) where a plugin's .slint file puts a `for` over an array it binds (an `in` or `in-out` array property) outside a `ListView`: such a `for` builds every row up front.
|
|
15
|
+
- **`null` for an image in a row** shows no picture (player 0.6.0), so rows without a thumbnail need no placeholder image.
|
|
16
|
+
- **`TestHost`** enforces the new limits and takes the split writes like the player (each line within `MESSAGE_LIMIT`, parts with `more` held until the last); `props` hold whole values as before.
|
|
17
|
+
|
|
3
18
|
## 1.5.45
|
|
4
19
|
|
|
5
20
|
**Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. Only when the SDK re-evaluates bindings changes; what a plugin's views and commands show stays the same. One case can show in tests: a plugin that changes state outside a handler (a timer, a promise continuation after the handler returned, a child process) without calling `plugin.update()`, and a test that relied on `host.setSource(...)` to refresh it. Have the plugin call `plugin.update()` after such a change (as the documentation always said), or wait `await host.settle(1000)` in the test; `settle()` does not wait for the once-a-second full pass.
|
package/bin/vestra-plugin.mjs
CHANGED
|
@@ -357,7 +357,10 @@ async function check(target, report = null) {
|
|
|
357
357
|
}
|
|
358
358
|
say(`${target.name}: capabilities declared for every API use`);
|
|
359
359
|
if (report) return;
|
|
360
|
-
for (const line of
|
|
360
|
+
for (const line of [
|
|
361
|
+
...(await bottomBarWarnings(target)),
|
|
362
|
+
...(await forWarnings(target)),
|
|
363
|
+
])
|
|
361
364
|
console.log(`${target.name}: warning: ${line}`);
|
|
362
365
|
// Translation warnings never fail the check.
|
|
363
366
|
if (present(join(target.directory, "locales"))) {
|
|
@@ -502,6 +505,61 @@ async function bottomBarWarnings(target) {
|
|
|
502
505
|
return warnings;
|
|
503
506
|
}
|
|
504
507
|
|
|
508
|
+
/**
|
|
509
|
+
* A `for` over an array the plugin binds (an `in` or `in-out` property of
|
|
510
|
+
* array type of a component or global in the same file), outside a
|
|
511
|
+
* `ListView`, in the plugin's .slint files: a reminder, not a failure
|
|
512
|
+
* (SDK 1.5.46). Such a `for` builds every row of the array up front; since
|
|
513
|
+
* player 0.6.0 a plugin may bind a whole long list, which belongs in a
|
|
514
|
+
* `ListView` (it builds only the rows on screen). A text scan: imported
|
|
515
|
+
* components and `@children` are not followed.
|
|
516
|
+
*/
|
|
517
|
+
async function forWarnings(target) {
|
|
518
|
+
const { slint } = await sourceFiles(target);
|
|
519
|
+
const warnings = [];
|
|
520
|
+
for (const file of slint) {
|
|
521
|
+
const text = slintCode(
|
|
522
|
+
await readFile(join(target.directory, file), "utf8"),
|
|
523
|
+
);
|
|
524
|
+
const arrays = new Set(
|
|
525
|
+
[
|
|
526
|
+
...text.matchAll(
|
|
527
|
+
/\b(?:in|in-out)\s+property\s*<\s*\[[^\]]*\]\s*>\s*([A-Za-z_][\w-]*)/g,
|
|
528
|
+
),
|
|
529
|
+
].map((match) => match[1]),
|
|
530
|
+
);
|
|
531
|
+
if (!arrays.size) continue;
|
|
532
|
+
// The elements open at each point: the name before each `{`.
|
|
533
|
+
const open = [];
|
|
534
|
+
let line = 1;
|
|
535
|
+
for (let i = 0; i < text.length; i++) {
|
|
536
|
+
const c = text[i];
|
|
537
|
+
if (c === "\n") line++;
|
|
538
|
+
else if (c === "{") {
|
|
539
|
+
const name = /([A-Za-z_][\w-]*)\s*$/.exec(
|
|
540
|
+
text.slice(Math.max(0, i - 80), i),
|
|
541
|
+
);
|
|
542
|
+
open.push(name ? name[1] : "");
|
|
543
|
+
} else if (c === "}") open.pop();
|
|
544
|
+
else if (
|
|
545
|
+
text.startsWith("for", i) &&
|
|
546
|
+
!/[\w-]/.test(text[i - 1] ?? " ") &&
|
|
547
|
+
/\s/.test(text[i + 3] ?? "")
|
|
548
|
+
) {
|
|
549
|
+
const loop =
|
|
550
|
+
/^for\s+[\w-]+(?:\s*\[\s*[\w-]+\s*\])?\s+in\s+([\w.-]+)/.exec(
|
|
551
|
+
text.slice(i, i + 200),
|
|
552
|
+
);
|
|
553
|
+
const array = loop?.[1].split(".").at(-1);
|
|
554
|
+
if (array && arrays.has(array) && !open.includes("ListView"))
|
|
555
|
+
warnings.push(
|
|
556
|
+
`${file.replaceAll("\\", "/")}:${line}: \`for\` over the bound array \`${array}\` outside a ListView builds every row; a long list should use a ListView (docs/reference/limits.md)`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return warnings;
|
|
562
|
+
}
|
|
505
563
|
/** The plugin's own production .ts / .mts and .slint files. */
|
|
506
564
|
async function sourceFiles(target) {
|
|
507
565
|
const all = await files(target.directory);
|
package/dist/plugin/patch.d.ts
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
export declare const PATCH_FEATURE = "ui.patch";
|
|
3
3
|
/** The most operations one patch carries (the player's `MAX_PATCH_OPS`). */
|
|
4
4
|
export declare const PATCH_OPS = 1024;
|
|
5
|
+
/**
|
|
6
|
+
* The most operations one diff plans (SDK 1.5.46): its patch is sent as
|
|
7
|
+
* several of at most {@link PATCH_OPS} each (`chunkOps`); more is sent whole
|
|
8
|
+
* (in parts, which the player writes at once).
|
|
9
|
+
*/
|
|
10
|
+
export declare const DIFF_OPS: number;
|
|
5
11
|
/**
|
|
6
12
|
* Fields that identify a row, in the order they are tried. A field is used
|
|
7
13
|
* when every row has it, as a string or a number, and no two rows share it.
|
|
@@ -61,7 +67,8 @@ export declare function countRow(row: unknown): RowCount;
|
|
|
61
67
|
export declare function withinLimits(counts: readonly RowCount[]): boolean;
|
|
62
68
|
/**
|
|
63
69
|
* The operations that turn the old rows into the new ones (`null`: more
|
|
64
|
-
* than {@link
|
|
70
|
+
* than {@link DIFF_OPS} would be needed, or more than {@link PATCH_OPS}
|
|
71
|
+
* moves), and for each new row the old row
|
|
65
72
|
* whose JSON it keeps (`-1`: a new or changed row). Keyed when both arrays
|
|
66
73
|
* have keys from the same field; otherwise by index.
|
|
67
74
|
*/
|
|
@@ -99,3 +106,47 @@ export declare function applyOps<T>(rows: T[], ops: readonly ({
|
|
|
99
106
|
count: number;
|
|
100
107
|
to: number;
|
|
101
108
|
})[]): T[];
|
|
109
|
+
/**
|
|
110
|
+
* The bytes of rows one write carries at most (SDK 1.5.46, player 0.6.0):
|
|
111
|
+
* half the 1 MiB protocol line, which leaves room for the request around
|
|
112
|
+
* them. A larger whole write is sent as its first rows and `insert`
|
|
113
|
+
* patches of the rest; a larger patch as several patches, each applying on
|
|
114
|
+
* the one before (the player's doc/architecture/large-lists.md §4).
|
|
115
|
+
*/
|
|
116
|
+
export declare const CHUNK_BYTES: number;
|
|
117
|
+
/** The bytes a row adds to a write (its JSON and a separator). */
|
|
118
|
+
export declare const rowBytes: (row: unknown) => number;
|
|
119
|
+
/**
|
|
120
|
+
* How many of the first rows (by their sizes, `rowBytes`) fit in `budget`
|
|
121
|
+
* bytes; at least one when there are rows (a row over the budget alone is
|
|
122
|
+
* left to the player's limits).
|
|
123
|
+
*/
|
|
124
|
+
export declare function prefixWithin(sizes: readonly number[], budget: number): number;
|
|
125
|
+
/** A wire operation (`set` and `insert` carry rows). */
|
|
126
|
+
export type WireOp = {
|
|
127
|
+
op: "set";
|
|
128
|
+
at: number;
|
|
129
|
+
row: unknown;
|
|
130
|
+
} | {
|
|
131
|
+
op: "insert";
|
|
132
|
+
at: number;
|
|
133
|
+
rows: unknown[];
|
|
134
|
+
} | {
|
|
135
|
+
op: "remove";
|
|
136
|
+
at: number;
|
|
137
|
+
count: number;
|
|
138
|
+
} | {
|
|
139
|
+
op: "move";
|
|
140
|
+
from: number;
|
|
141
|
+
count: number;
|
|
142
|
+
to: number;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* `ops` as consecutive patches of at most {@link CHUNK_BYTES} of rows and
|
|
146
|
+
* {@link PATCH_OPS} operations each, in order: applying them one after the
|
|
147
|
+
* other gives what `ops` give. An `insert` of more rows than fit is split
|
|
148
|
+
* into inserts of consecutive runs.
|
|
149
|
+
*/
|
|
150
|
+
export declare function chunkOps<T extends WireOp>(ops: readonly T[]): T[][];
|
|
151
|
+
/** The bytes a row already in JSON adds to a write (and a separator). */
|
|
152
|
+
export declare const jsonBytes: (json: string) => number;
|
package/dist/plugin/patch.js
CHANGED
|
@@ -9,6 +9,12 @@ import { UI_VALUE_LIMITS } from "./values.js";
|
|
|
9
9
|
export const PATCH_FEATURE = "ui.patch";
|
|
10
10
|
/** The most operations one patch carries (the player's `MAX_PATCH_OPS`). */
|
|
11
11
|
export const PATCH_OPS = 1024;
|
|
12
|
+
/**
|
|
13
|
+
* The most operations one diff plans (SDK 1.5.46): its patch is sent as
|
|
14
|
+
* several of at most {@link PATCH_OPS} each (`chunkOps`); more is sent whole
|
|
15
|
+
* (in parts, which the player writes at once).
|
|
16
|
+
*/
|
|
17
|
+
export const DIFF_OPS = 16 * PATCH_OPS;
|
|
12
18
|
/**
|
|
13
19
|
* Fields that identify a row, in the order they are tried. A field is used
|
|
14
20
|
* when every row has it, as a string or a number, and no two rows share it.
|
|
@@ -159,7 +165,8 @@ function increasing(values) {
|
|
|
159
165
|
}
|
|
160
166
|
/**
|
|
161
167
|
* The operations that turn the old rows into the new ones (`null`: more
|
|
162
|
-
* than {@link
|
|
168
|
+
* than {@link DIFF_OPS} would be needed, or more than {@link PATCH_OPS}
|
|
169
|
+
* moves), and for each new row the old row
|
|
163
170
|
* whose JSON it keeps (`-1`: a new or changed row). Keyed when both arrays
|
|
164
171
|
* have keys from the same field; otherwise by index.
|
|
165
172
|
*/
|
|
@@ -168,7 +175,7 @@ export function diff(old, next) {
|
|
|
168
175
|
const ops = old.keys && next.keys
|
|
169
176
|
? keyed(old.enc, old.keys, next.enc, next.keys, from)
|
|
170
177
|
: byIndex(old.enc, next.enc, from);
|
|
171
|
-
return ops && ops.length <=
|
|
178
|
+
return ops && ops.length <= DIFF_OPS ? { ops, from } : null;
|
|
172
179
|
}
|
|
173
180
|
function byIndex(old, next, from) {
|
|
174
181
|
const min = Math.min(old.length, next.length);
|
|
@@ -236,7 +243,7 @@ function keyed(oldEnc, oldKeys, newEnc, newKeys, from) {
|
|
|
236
243
|
}
|
|
237
244
|
else
|
|
238
245
|
ops.push({ op: "remove", at: i, count: 1 });
|
|
239
|
-
if (ops.length >
|
|
246
|
+
if (ops.length > DIFF_OPS)
|
|
240
247
|
return null;
|
|
241
248
|
}
|
|
242
249
|
for (let i = p; i < oldEnd; i++)
|
|
@@ -248,7 +255,10 @@ function keyed(oldEnc, oldKeys, newEnc, newKeys, from) {
|
|
|
248
255
|
// positions goes right after the kept row before it in the new order.
|
|
249
256
|
const positions = kept.map((key) => target.get(key));
|
|
250
257
|
const stay = increasing(positions);
|
|
251
|
-
|
|
258
|
+
// Moves cost a search each: at most PATCH_OPS of them.
|
|
259
|
+
if (kept.length - stay.size > PATCH_OPS)
|
|
260
|
+
return null;
|
|
261
|
+
if (kept.length - stay.size + ops.length > DIFF_OPS)
|
|
252
262
|
return null;
|
|
253
263
|
const stays = new Set();
|
|
254
264
|
for (const index of stay)
|
|
@@ -281,13 +291,13 @@ function keyed(oldEnc, oldKeys, newEnc, newKeys, from) {
|
|
|
281
291
|
last.rows.push(j);
|
|
282
292
|
else
|
|
283
293
|
ops.push({ op: "insert", at: j, rows: [j] });
|
|
284
|
-
if (ops.length >
|
|
294
|
+
if (ops.length > DIFF_OPS)
|
|
285
295
|
return null;
|
|
286
296
|
}
|
|
287
297
|
sets.sort((a, b) => a.at - b.at);
|
|
288
298
|
for (const set of sets) {
|
|
289
299
|
ops.push(set);
|
|
290
|
-
if (ops.length >
|
|
300
|
+
if (ops.length > DIFF_OPS)
|
|
291
301
|
return null;
|
|
292
302
|
}
|
|
293
303
|
return ops;
|
|
@@ -335,3 +345,91 @@ export function applyOps(rows, ops) {
|
|
|
335
345
|
}
|
|
336
346
|
return out;
|
|
337
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* The bytes of rows one write carries at most (SDK 1.5.46, player 0.6.0):
|
|
350
|
+
* half the 1 MiB protocol line, which leaves room for the request around
|
|
351
|
+
* them. A larger whole write is sent as its first rows and `insert`
|
|
352
|
+
* patches of the rest; a larger patch as several patches, each applying on
|
|
353
|
+
* the one before (the player's doc/architecture/large-lists.md §4).
|
|
354
|
+
*/
|
|
355
|
+
export const CHUNK_BYTES = 512 * 1024;
|
|
356
|
+
/** About what an operation adds to a request besides its rows. */
|
|
357
|
+
const OP_BYTES = 64;
|
|
358
|
+
/** UTF-8 bytes of `text` (JSON of a row), without encoding it. */
|
|
359
|
+
function utf8Bytes(text) {
|
|
360
|
+
let bytes = text.length;
|
|
361
|
+
for (let i = 0; i < text.length; i++) {
|
|
362
|
+
const code = text.charCodeAt(i);
|
|
363
|
+
if (code >= 0x80)
|
|
364
|
+
bytes += code >= 0x800 && (code < 0xd800 || code > 0xdfff) ? 2 : 1;
|
|
365
|
+
}
|
|
366
|
+
return bytes;
|
|
367
|
+
}
|
|
368
|
+
/** The bytes a row adds to a write (its JSON and a separator). */
|
|
369
|
+
export const rowBytes = (row) => utf8Bytes(JSON.stringify(row) ?? "null") + 1;
|
|
370
|
+
/**
|
|
371
|
+
* How many of the first rows (by their sizes, `rowBytes`) fit in `budget`
|
|
372
|
+
* bytes; at least one when there are rows (a row over the budget alone is
|
|
373
|
+
* left to the player's limits).
|
|
374
|
+
*/
|
|
375
|
+
export function prefixWithin(sizes, budget) {
|
|
376
|
+
let used = 0;
|
|
377
|
+
for (let i = 0; i < sizes.length; i++) {
|
|
378
|
+
used += sizes[i];
|
|
379
|
+
if (used > budget)
|
|
380
|
+
return Math.max(i, 1);
|
|
381
|
+
}
|
|
382
|
+
return sizes.length;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* `ops` as consecutive patches of at most {@link CHUNK_BYTES} of rows and
|
|
386
|
+
* {@link PATCH_OPS} operations each, in order: applying them one after the
|
|
387
|
+
* other gives what `ops` give. An `insert` of more rows than fit is split
|
|
388
|
+
* into inserts of consecutive runs.
|
|
389
|
+
*/
|
|
390
|
+
export function chunkOps(ops) {
|
|
391
|
+
const groups = [];
|
|
392
|
+
let group = [];
|
|
393
|
+
let bytes = 0;
|
|
394
|
+
const push = (op, size) => {
|
|
395
|
+
if (group.length &&
|
|
396
|
+
(bytes + size > CHUNK_BYTES || group.length >= PATCH_OPS)) {
|
|
397
|
+
groups.push(group);
|
|
398
|
+
group = [];
|
|
399
|
+
bytes = 0;
|
|
400
|
+
}
|
|
401
|
+
group.push(op);
|
|
402
|
+
bytes += size;
|
|
403
|
+
};
|
|
404
|
+
for (const op of ops) {
|
|
405
|
+
if (op.op !== "insert") {
|
|
406
|
+
push(op, OP_BYTES + (op.op === "set" ? rowBytes(op.row) : 0));
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const sizes = op.rows.map(rowBytes);
|
|
410
|
+
let start = 0;
|
|
411
|
+
while (start < sizes.length) {
|
|
412
|
+
const room = Math.max(CHUNK_BYTES - bytes - OP_BYTES, 0);
|
|
413
|
+
let count = prefixWithin(sizes.slice(start), room);
|
|
414
|
+
// Nothing fits beside this group's rows: the run starts a new patch.
|
|
415
|
+
if (room === 0 ||
|
|
416
|
+
sizes.slice(start, start + count).reduce((a, b) => a + b, 0) > room) {
|
|
417
|
+
if (group.length) {
|
|
418
|
+
groups.push(group);
|
|
419
|
+
group = [];
|
|
420
|
+
bytes = 0;
|
|
421
|
+
}
|
|
422
|
+
count = prefixWithin(sizes.slice(start), CHUNK_BYTES - OP_BYTES);
|
|
423
|
+
}
|
|
424
|
+
const rows = op.rows.slice(start, start + count);
|
|
425
|
+
const size = OP_BYTES + sizes.slice(start, start + count).reduce((a, b) => a + b, 0);
|
|
426
|
+
push({ op: "insert", at: op.at + start, rows }, size);
|
|
427
|
+
start += count;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (group.length)
|
|
431
|
+
groups.push(group);
|
|
432
|
+
return groups;
|
|
433
|
+
}
|
|
434
|
+
/** The bytes a row already in JSON adds to a write (and a separator). */
|
|
435
|
+
export const jsonBytes = (json) => utf8Bytes(json) + 1;
|
package/dist/plugin/testing.js
CHANGED
|
@@ -935,10 +935,15 @@ export class TestHost {
|
|
|
935
935
|
* view and property (none: 0, the initial properties).
|
|
936
936
|
*/
|
|
937
937
|
#writes = new Map();
|
|
938
|
+
/** The parts of a whole value held until its last part, by view and property. */
|
|
939
|
+
#parts = new Map();
|
|
938
940
|
#resetWrites(view) {
|
|
939
941
|
for (const slot of [...this.#writes.keys()])
|
|
940
942
|
if (slot.startsWith(`${view}\u0000`))
|
|
941
943
|
this.#writes.delete(slot);
|
|
944
|
+
for (const slot of [...this.#parts.keys()])
|
|
945
|
+
if (slot.startsWith(`${view}\u0000`))
|
|
946
|
+
this.#parts.delete(slot);
|
|
942
947
|
}
|
|
943
948
|
/**
|
|
944
949
|
* A property write as the player takes it, returning the value the
|
|
@@ -950,6 +955,44 @@ export class TestHost {
|
|
|
950
955
|
#write(view, property, p, current) {
|
|
951
956
|
const slot = `${view}\u0000${property}`;
|
|
952
957
|
const patch = p.patch;
|
|
958
|
+
// A whole array sent in parts (player 0.6.0, SDK 1.5.46): every part but
|
|
959
|
+
// the last carries `more`; the player holds them and writes the whole
|
|
960
|
+
// value at the last, as here (`undefined`: held, nothing shown yet).
|
|
961
|
+
const more = p.more === true;
|
|
962
|
+
const held = this.#parts.get(slot);
|
|
963
|
+
this.#parts.delete(slot);
|
|
964
|
+
if (more && patch === undefined) {
|
|
965
|
+
const rows = this.#shownValues(p.value);
|
|
966
|
+
if (!Array.isArray(rows))
|
|
967
|
+
throw new HostError("invalid", "a value sent in parts is an array");
|
|
968
|
+
noMarkdownParts(rows);
|
|
969
|
+
this.#checkValues({ [property]: rows });
|
|
970
|
+
this.#parts.set(slot, {
|
|
971
|
+
seq: typeof p.seq === "number" ? p.seq : 0,
|
|
972
|
+
rows,
|
|
973
|
+
});
|
|
974
|
+
return undefined;
|
|
975
|
+
}
|
|
976
|
+
if (patch !== undefined && held && patch.base === held.seq) {
|
|
977
|
+
const ops = patchOps(patch.ops, held.rows.length);
|
|
978
|
+
const resolved = ops.map((op) => op.op === "set"
|
|
979
|
+
? { ...op, row: this.#shownValues(op.row) }
|
|
980
|
+
: op.op === "insert"
|
|
981
|
+
? { ...op, rows: op.rows.map((row) => this.#shownValues(row)) }
|
|
982
|
+
: op);
|
|
983
|
+
noMarkdownParts(resolved.flatMap((op) => op.op === "set" ? [op.row] : op.op === "insert" ? op.rows : []));
|
|
984
|
+
const rows = applyOps(held.rows, resolved);
|
|
985
|
+
this.#checkValues({ [property]: rows });
|
|
986
|
+
if (more) {
|
|
987
|
+
this.#parts.set(slot, { seq: patch.seq, rows });
|
|
988
|
+
return undefined;
|
|
989
|
+
}
|
|
990
|
+
this.#seeImages([rows]);
|
|
991
|
+
this.#writes.set(slot, patch.seq);
|
|
992
|
+
return rows;
|
|
993
|
+
}
|
|
994
|
+
if (patch !== undefined && more)
|
|
995
|
+
throw new HostError("stale", "the property changed since the write this patch applies on; send the whole value");
|
|
953
996
|
if (patch === undefined) {
|
|
954
997
|
const value = this.#shownValues(p.value);
|
|
955
998
|
this.#checkValues({ [property]: value });
|
|
@@ -3899,7 +3942,10 @@ export class TestHost {
|
|
|
3899
3942
|
if (!window)
|
|
3900
3943
|
throw new HostError("missing", "view not found");
|
|
3901
3944
|
const property = String(p.property);
|
|
3902
|
-
|
|
3945
|
+
const next = this.#write(`w:${String(p.id)}`, property, p, window.props[property]);
|
|
3946
|
+
// A part of a value sent in parts is held until the last (player 0.6.0).
|
|
3947
|
+
if (next !== undefined)
|
|
3948
|
+
window.props[property] = next;
|
|
3903
3949
|
},
|
|
3904
3950
|
"ui.show": (p) => {
|
|
3905
3951
|
const window = this.windows.get(String(p.id));
|
|
@@ -4260,7 +4306,10 @@ export class TestHost {
|
|
|
4260
4306
|
"ui.setMount": (p) => {
|
|
4261
4307
|
const panel = this.mounted();
|
|
4262
4308
|
const property = String(p.property);
|
|
4263
|
-
|
|
4309
|
+
const next = this.#write("$mount", property, p, panel.props[property]);
|
|
4310
|
+
// A part of a value sent in parts is held until the last (player 0.6.0).
|
|
4311
|
+
if (next !== undefined)
|
|
4312
|
+
panel.props[property] = next;
|
|
4264
4313
|
},
|
|
4265
4314
|
"timeline.marks": () => ({ marks: this.timeline.marks }),
|
|
4266
4315
|
"drops.provide": (p) => {
|
|
@@ -4323,7 +4372,10 @@ export class TestHost {
|
|
|
4323
4372
|
if (!page)
|
|
4324
4373
|
throw new HostError("missing", "page not registered");
|
|
4325
4374
|
const property = String(p.property);
|
|
4326
|
-
|
|
4375
|
+
const next = this.#write(`$settings:${String(p.id)}`, property, p, page.props[property]);
|
|
4376
|
+
// A part of a value sent in parts is held until the last (player 0.6.0).
|
|
4377
|
+
if (next !== undefined)
|
|
4378
|
+
page.props[property] = next;
|
|
4327
4379
|
},
|
|
4328
4380
|
"settings.unregister": (p) => {
|
|
4329
4381
|
this.settings.pages.delete(String(p.id));
|
|
@@ -5352,6 +5404,11 @@ export async function startPlugin(activate, options = {}) {
|
|
|
5352
5404
|
* before; at most {@link PATCH_OPS}; the result at most
|
|
5353
5405
|
* `UI_VALUE_LIMITS.arrayRows` rows.
|
|
5354
5406
|
*/
|
|
5407
|
+
/** The player takes no Markdown documents in parts of a value (`invalid`). */
|
|
5408
|
+
function noMarkdownParts(rows) {
|
|
5409
|
+
if (JSON.stringify(rows).includes('"$vestraMarkdown"'))
|
|
5410
|
+
throw new HostError("invalid", "a value sent in parts holds no Markdown documents");
|
|
5411
|
+
}
|
|
5355
5412
|
function patchOps(ops, length) {
|
|
5356
5413
|
if (!Array.isArray(ops))
|
|
5357
5414
|
throw new HostError("invalid", "patch.ops requires an array");
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -1384,20 +1384,23 @@ export interface MarkdownOptions {
|
|
|
1384
1384
|
* thumbnails.
|
|
1385
1385
|
*
|
|
1386
1386
|
* Limits per value (`UI_VALUE_LIMITS` in `@vestra-player/sdk/testing`,
|
|
1387
|
-
* player 0.4.3
|
|
1388
|
-
*
|
|
1389
|
-
* null counts, the value itself included; a
|
|
1390
|
-
*
|
|
1391
|
-
* field
|
|
1392
|
-
* document). A list of rows
|
|
1393
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
*
|
|
1396
|
-
*
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1387
|
+
* player 0.6.0; players 0.4.3 to 0.5: 16384 values, 4096 rows, 1 MiB of
|
|
1388
|
+
* text): nesting depth 8, 1,048,576 values in total (every array, object,
|
|
1389
|
+
* string, number, boolean and null counts, the value itself included; a
|
|
1390
|
+
* document or an image is one), 100,000 rows per array, 64 KiB per string
|
|
1391
|
+
* or field name and 32 MiB of text in total, field names and the text of
|
|
1392
|
+
* its Markdown documents included (256 KB per document). A list of rows
|
|
1393
|
+
* with 13 fields is 14 values per row, so 50,000 such rows fit. An array
|
|
1394
|
+
* property larger than one 1 MiB request is sent in parts (SDK 1.5.46, a
|
|
1395
|
+
* player that takes row patches); only the top-level array is split, so
|
|
1396
|
+
* any other value, and each row, must fit in one request (a row of more
|
|
1397
|
+
* than 512 KiB is refused with `limit`). A value over a limit is refused
|
|
1398
|
+
* with code
|
|
1399
|
+
* `limit` and the property keeps what it showed (see {@link View.set});
|
|
1400
|
+
* `TestHost` refuses it the same way.
|
|
1401
|
+
*
|
|
1402
|
+
* Bind a whole list and draw it with a Slint `ListView`, which builds only
|
|
1403
|
+
* the rows on screen; the SDK sends what changed (row patches).
|
|
1401
1404
|
*
|
|
1402
1405
|
* @since 1.0.0
|
|
1403
1406
|
* @category UI
|
package/dist/plugin/ui.d.ts
CHANGED
|
@@ -32,9 +32,15 @@ export declare abstract class ViewBase implements View {
|
|
|
32
32
|
protected abstract write(property: string, write: PropertyWrite): Promise<void>;
|
|
33
33
|
/**
|
|
34
34
|
* Initial properties in wire form. A released image is left out (the
|
|
35
|
-
* property keeps its default) and reported, like a failed write.
|
|
35
|
+
* property keeps its default) and reported, like a failed write. With a
|
|
36
|
+
* player that takes row patches, the arrays of one request carry at most
|
|
37
|
+
* {@link CHUNK_BYTES} of rows together (SDK 1.5.46): an array over that
|
|
38
|
+
* goes with its first rows, and the first flush sends the rest as patches.
|
|
39
|
+
* An array over the value limits is refused here, as the player would
|
|
40
|
+
* refuse it whole: left out and reported (a panel, a settings page), or,
|
|
41
|
+
* with `refuse` (a window's `show`), thrown.
|
|
36
42
|
*/
|
|
37
|
-
protected wired(values: Record<string, PropValue
|
|
43
|
+
protected wired(values: Record<string, PropValue>, refuse?: boolean): Record<string, Json>;
|
|
38
44
|
on<A extends Json[]>(callback: string, handler: (...args: A) => unknown): Disposable;
|
|
39
45
|
set(props: Props): void;
|
|
40
46
|
bind(props: () => Props): Disposable;
|
package/dist/plugin/ui.js
CHANGED
|
@@ -7,7 +7,8 @@ import { failure, Latest, quiet } from "./runtime.js";
|
|
|
7
7
|
import { dropsOf } from "./drops.js";
|
|
8
8
|
import { MarkdownHandle, markdown, remoteImageFile } from "./markdown.js";
|
|
9
9
|
import { HostError } from "../wire.js";
|
|
10
|
-
import { countRow, diff, keyOf, PATCH_FEATURE, replay, withinLimits, } from "./patch.js";
|
|
10
|
+
import { countRow, diff, keyOf, CHUNK_BYTES, chunkOps, PATCH_FEATURE, prefixWithin, jsonBytes, rowBytes, replay, withinLimits, } from "./patch.js";
|
|
11
|
+
import { UI_VALUE_LIMITS, valueLimitProblem } from "./values.js";
|
|
11
12
|
/** Slint names are kebab-case; plugin code may use camelCase. */
|
|
12
13
|
export const kebab = (name) => name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
|
|
13
14
|
const encode = (props) => Object.fromEntries(Object.entries(props).map(([k, v]) => [kebab(k), v]));
|
|
@@ -110,6 +111,26 @@ function sentOf(value, patches) {
|
|
|
110
111
|
}
|
|
111
112
|
return { kind: "json", enc: JSON.stringify(value) };
|
|
112
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* A write sent in parts stopped because its view closed or showed its
|
|
116
|
+
* initial properties again: not a failure.
|
|
117
|
+
*/
|
|
118
|
+
class Abandoned extends Error {
|
|
119
|
+
}
|
|
120
|
+
/** The wire key of a Markdown document: rows holding one are never sent in parts. */ const MARKDOWN_KEY = '"$vestraMarkdown"';
|
|
121
|
+
/** A row larger than one request can carry (`limit`). */
|
|
122
|
+
const rowTooLarge = (key, bytes) => new HostError("limit", `${key}: a row of ${bytes} bytes is larger than one request takes (${CHUNK_BYTES} bytes)`);
|
|
123
|
+
/** Refuse a patch carrying a row larger than one request, before sending it. */
|
|
124
|
+
function tooLarge(key, ops) {
|
|
125
|
+
for (const op of ops) {
|
|
126
|
+
const rows = op.op === "set" ? [op.row] : op.op === "insert" ? op.rows : [];
|
|
127
|
+
for (const row of rows) {
|
|
128
|
+
const bytes = rowBytes(row);
|
|
129
|
+
if (bytes > CHUNK_BYTES)
|
|
130
|
+
throw rowTooLarge(key, bytes);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
113
134
|
/**
|
|
114
135
|
* The row patch from `sent` to `next` (the bound `rows`), in wire form, and
|
|
115
136
|
* the new rows' limit counts; `null` when the whole value is to be sent:
|
|
@@ -194,6 +215,8 @@ export class ViewBase {
|
|
|
194
215
|
*/
|
|
195
216
|
#stales = new Map();
|
|
196
217
|
#seq = 0;
|
|
218
|
+
/** Counts the times the view showed its initial properties (`shown`). */
|
|
219
|
+
#generation = 0;
|
|
197
220
|
/** Written by `set()`: sent even if equal to what was sent before. */
|
|
198
221
|
#forced = new Set();
|
|
199
222
|
/** The last value of each property whose write failed; not retried until it changes. */
|
|
@@ -215,18 +238,51 @@ export class ViewBase {
|
|
|
215
238
|
}
|
|
216
239
|
/**
|
|
217
240
|
* Initial properties in wire form. A released image is left out (the
|
|
218
|
-
* property keeps its default) and reported, like a failed write.
|
|
241
|
+
* property keeps its default) and reported, like a failed write. With a
|
|
242
|
+
* player that takes row patches, the arrays of one request carry at most
|
|
243
|
+
* {@link CHUNK_BYTES} of rows together (SDK 1.5.46): an array over that
|
|
244
|
+
* goes with its first rows, and the first flush sends the rest as patches.
|
|
245
|
+
* An array over the value limits is refused here, as the player would
|
|
246
|
+
* refuse it whole: left out and reported (a panel, a settings page), or,
|
|
247
|
+
* with `refuse` (a window's `show`), thrown.
|
|
219
248
|
*/
|
|
220
|
-
wired(values) {
|
|
249
|
+
wired(values, refuse = false) {
|
|
221
250
|
const out = {};
|
|
251
|
+
let room = CHUNK_BYTES;
|
|
222
252
|
for (const [key, value] of Object.entries(values)) {
|
|
253
|
+
let wire;
|
|
223
254
|
try {
|
|
224
|
-
|
|
255
|
+
wire = wireValue(value);
|
|
225
256
|
}
|
|
226
257
|
catch (error) {
|
|
227
258
|
this.#failed.set(key, JSON.stringify(value));
|
|
228
259
|
this.scope.report(error);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (this.#patches && Array.isArray(wire)) {
|
|
263
|
+
const sizes = wire.map(rowBytes);
|
|
264
|
+
const total = sizes.reduce((a, b) => a + b, 0);
|
|
265
|
+
if (total > room && !JSON.stringify(wire).includes(MARKDOWN_KEY)) {
|
|
266
|
+
const largest = sizes.reduce((a, b) => Math.max(a, b), 0);
|
|
267
|
+
const problem = valueLimitProblem(wire);
|
|
268
|
+
if (problem || largest > CHUNK_BYTES) {
|
|
269
|
+
const error = problem
|
|
270
|
+
? new HostError("limit", `${key}: ${problem}`)
|
|
271
|
+
: rowTooLarge(key, largest);
|
|
272
|
+
if (refuse)
|
|
273
|
+
throw error;
|
|
274
|
+
this.#failed.set(key, JSON.stringify(value));
|
|
275
|
+
this.scope.report(error);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const count = room > 0 ? prefixWithin(sizes, room) : 0;
|
|
279
|
+
wire = wire.slice(0, count);
|
|
280
|
+
room -= sizes.slice(0, count).reduce((a, b) => a + b, 0);
|
|
281
|
+
}
|
|
282
|
+
else
|
|
283
|
+
room -= total;
|
|
229
284
|
}
|
|
285
|
+
out[key] = wire;
|
|
230
286
|
}
|
|
231
287
|
return out;
|
|
232
288
|
}
|
|
@@ -289,6 +345,7 @@ export class ViewBase {
|
|
|
289
345
|
}
|
|
290
346
|
/** The host already shows these values (e.g. sent as initial properties). */
|
|
291
347
|
shown(values) {
|
|
348
|
+
this.#generation++;
|
|
292
349
|
const patches = this.#patches;
|
|
293
350
|
this.#sent = new Map(Object.entries(values).map(([k, v]) => [k, sentOf(v, patches)]));
|
|
294
351
|
this.#bases = new Map(patches ? Object.keys(values).map((key) => [key, 0]) : []);
|
|
@@ -354,7 +411,10 @@ export class ViewBase {
|
|
|
354
411
|
* Write one property: a row patch when the player takes them and one
|
|
355
412
|
* applies (else the whole value); a `stale` patch is followed by the whole
|
|
356
413
|
* value at once. The base is forgotten on any failure, so the next write
|
|
357
|
-
* of the property is whole.
|
|
414
|
+
* of the property is whole. A write larger than one request
|
|
415
|
+
* ({@link CHUNK_BYTES} of rows, SDK 1.5.46) is sent in parts: a whole
|
|
416
|
+
* array as its first rows and `insert` patches of the rest, a patch as
|
|
417
|
+
* several, each applying on the one before.
|
|
358
418
|
*/
|
|
359
419
|
async #send(pending) {
|
|
360
420
|
const { key, value } = pending;
|
|
@@ -362,34 +422,121 @@ export class ViewBase {
|
|
|
362
422
|
await this.write(key, { value: wireValue(value) });
|
|
363
423
|
return pending.sent();
|
|
364
424
|
}
|
|
425
|
+
const generation = this.#generation;
|
|
365
426
|
const base = this.#bases.get(key);
|
|
366
427
|
this.#bases.delete(key);
|
|
367
428
|
const sent = this.#sent.get(key);
|
|
368
|
-
|
|
429
|
+
let plan = base !== undefined &&
|
|
369
430
|
sent?.kind === "rows" &&
|
|
370
431
|
pending.rows &&
|
|
371
432
|
(this.#stales.get(key) ?? 0) < 2
|
|
372
433
|
? rowPatch(sent.rows, pending.rows, value)
|
|
373
434
|
: null;
|
|
435
|
+
// A patch sent in several never passes the row limit midway (the diff
|
|
436
|
+
// removes before it inserts; checked anyway); otherwise whole.
|
|
437
|
+
if (plan && sent?.kind === "rows") {
|
|
438
|
+
let length = sent.rows.enc.length;
|
|
439
|
+
for (const op of plan.ops) {
|
|
440
|
+
if (op.op === "insert")
|
|
441
|
+
length += op.rows.length;
|
|
442
|
+
else if (op.op === "remove")
|
|
443
|
+
length -= op.count;
|
|
444
|
+
if (length > UI_VALUE_LIMITS.arrayRows) {
|
|
445
|
+
plan = null;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
374
450
|
if (plan) {
|
|
451
|
+
tooLarge(key, plan.ops);
|
|
452
|
+
const [first, ...rest] = chunkOps(plan.ops);
|
|
375
453
|
const seq = ++this.#seq;
|
|
454
|
+
let applied = false;
|
|
376
455
|
try {
|
|
377
|
-
await this.write(key, { patch: { base: base, seq, ops:
|
|
378
|
-
|
|
379
|
-
this.#stales.delete(key);
|
|
380
|
-
return pending.sent(plan.counts);
|
|
456
|
+
await this.write(key, { patch: { base: base, seq, ops: first } });
|
|
457
|
+
applied = true;
|
|
381
458
|
}
|
|
382
459
|
catch (error) {
|
|
383
460
|
if (!(error instanceof HostError && error.code === "stale"))
|
|
384
461
|
throw error;
|
|
385
462
|
this.#stales.set(key, (this.#stales.get(key) ?? 0) + 1);
|
|
386
463
|
}
|
|
464
|
+
if (applied) {
|
|
465
|
+
this.#stales.delete(key);
|
|
466
|
+
await this.#parts(key, seq, rest, false, generation);
|
|
467
|
+
return pending.sent(plan.counts);
|
|
468
|
+
}
|
|
387
469
|
}
|
|
388
470
|
const seq = ++this.#seq;
|
|
471
|
+
// Rows holding Markdown documents go whole, as before: the player
|
|
472
|
+
// merges documents in place and takes no parts of them.
|
|
473
|
+
if (Array.isArray(value) &&
|
|
474
|
+
pending.rows &&
|
|
475
|
+
!pending.rows.enc.some((row) => row.includes(MARKDOWN_KEY))) {
|
|
476
|
+
const sizes = pending.rows.enc.map(jsonBytes);
|
|
477
|
+
const count = prefixWithin(sizes, CHUNK_BYTES);
|
|
478
|
+
if (count < value.length) {
|
|
479
|
+
const rows = value;
|
|
480
|
+
// Over the limits, or a row larger than one request: refused here,
|
|
481
|
+
// as the player would refuse the whole value, before any part is
|
|
482
|
+
// sent (a part refused midway would leave the list half sent).
|
|
483
|
+
const problem = valueLimitProblem(wireValue(value));
|
|
484
|
+
if (problem)
|
|
485
|
+
throw new HostError("limit", `${key}: ${problem}`);
|
|
486
|
+
const largest = sizes.reduce((a, b) => Math.max(a, b), 0);
|
|
487
|
+
if (largest > CHUNK_BYTES)
|
|
488
|
+
throw rowTooLarge(key, largest);
|
|
489
|
+
// Every part but the last carries `more`: the player holds them
|
|
490
|
+
// and writes the whole value at the last, so a list it shows is
|
|
491
|
+
// written once (player 0.6.0).
|
|
492
|
+
await this.write(key, {
|
|
493
|
+
value: wireValue(rows.slice(0, count)),
|
|
494
|
+
seq,
|
|
495
|
+
more: true,
|
|
496
|
+
});
|
|
497
|
+
const insert = {
|
|
498
|
+
op: "insert",
|
|
499
|
+
at: count,
|
|
500
|
+
rows: rows.slice(count).map((row) => wireValue(row ?? null)),
|
|
501
|
+
};
|
|
502
|
+
await this.#parts(key, seq, chunkOps([insert]), true, generation);
|
|
503
|
+
return pending.sent();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
389
506
|
await this.write(key, { value: wireValue(value), seq });
|
|
390
507
|
this.#bases.set(key, seq);
|
|
391
508
|
return pending.sent();
|
|
392
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* The rest of a write sent in parts: each patch applies on the one
|
|
512
|
+
* before (`base`), and the property's base is the last one's; with
|
|
513
|
+
* `more`, every part but the last says so. A failure leaves no base: the
|
|
514
|
+
* next write of the property is whole. When the view closed or showed its
|
|
515
|
+
* initial properties again meanwhile (`generation`), the rest is dropped
|
|
516
|
+
* quietly ({@link Abandoned}): the view's next flush sends what it shows.
|
|
517
|
+
*/
|
|
518
|
+
async #parts(key, base, groups, more, generation) {
|
|
519
|
+
let last = base;
|
|
520
|
+
for (const [i, ops] of groups.entries()) {
|
|
521
|
+
if (!this.live() || this.#generation !== generation)
|
|
522
|
+
throw new Abandoned();
|
|
523
|
+
const seq = ++this.#seq;
|
|
524
|
+
try {
|
|
525
|
+
await this.write(key, {
|
|
526
|
+
patch: { base: last, seq, ops },
|
|
527
|
+
...(more && i < groups.length - 1 ? { more: true } : {}),
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
// Refused because the view went meanwhile: not a failure.
|
|
532
|
+
if (!this.live() || this.#generation !== generation)
|
|
533
|
+
throw new Abandoned();
|
|
534
|
+
throw error;
|
|
535
|
+
}
|
|
536
|
+
last = seq;
|
|
537
|
+
}
|
|
538
|
+
this.#bases.set(key, last);
|
|
539
|
+
}
|
|
393
540
|
/**
|
|
394
541
|
* Send what changed. A bound value is sent when it differs from the last one
|
|
395
542
|
* sent; a `set()` value is always sent once. Nothing is resent merely
|
|
@@ -431,6 +578,13 @@ export class ViewBase {
|
|
|
431
578
|
this.#broken = false;
|
|
432
579
|
}
|
|
433
580
|
catch (error) {
|
|
581
|
+
// The view closed or showed its initial properties again while
|
|
582
|
+
// a write was sent in parts: nothing failed (the next flush
|
|
583
|
+
// sends what the view shows).
|
|
584
|
+
if (error instanceof Abandoned) {
|
|
585
|
+
queueMicrotask(() => this.flush());
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
434
588
|
failed = true;
|
|
435
589
|
// Report the start of a failing streak only.
|
|
436
590
|
if (!this.#broken)
|
|
@@ -530,7 +684,7 @@ class WindowHandle extends ViewBase {
|
|
|
530
684
|
const ui = this.scope.runtime.host.ui;
|
|
531
685
|
if (this.#opened)
|
|
532
686
|
return ui.show(this.#id, true);
|
|
533
|
-
const initial = this.wired(this.snapshot());
|
|
687
|
+
const initial = this.wired(this.snapshot(), true);
|
|
534
688
|
await ui.open(this.#id, this.#file, initial, [...this.handlers.keys()]);
|
|
535
689
|
this.#opened = true;
|
|
536
690
|
this.shown(initial);
|
package/dist/plugin/values.d.ts
CHANGED
|
@@ -12,41 +12,26 @@ import type { Json } from "../wire.ts";
|
|
|
12
12
|
* makes `Window.show` reject; a panel or settings page opens without it and
|
|
13
13
|
* the failure goes to the plugin's `onError`.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Since player 0.6.0 (SDK 1.5.46) they allow a whole list: a plugin binds
|
|
16
|
+
* all of its rows (a playlist of 50,000 entries fits) and the view draws
|
|
17
|
+
* them with a Slint `ListView`, which builds only the rows on screen. The
|
|
18
|
+
* SDK sends a changed array as row patches, and an array larger than one
|
|
19
|
+
* request (about 512 KiB of rows) as its first rows followed by `insert`
|
|
20
|
+
* patches of the rest; the player converts large writes off its interface
|
|
21
|
+
* thread. Before player 0.6.0 the limits were 4,096 rows, 16,384 values and
|
|
22
|
+
* 1 MiB of text, so a long list had to be sent as a window of rows.
|
|
23
|
+
*
|
|
24
|
+
* In a row, `null` where the view declares an `image` shows no picture
|
|
25
|
+
* (player 0.6.0): rows without a thumbnail need no placeholder image.
|
|
26
|
+
*
|
|
27
|
+
* @example Bind a whole list
|
|
28
|
+
* The view draws every row with a `ListView`; the SDK sends what changed.
|
|
19
29
|
*
|
|
20
|
-
* @example Send a long playlist as a window of rows
|
|
21
|
-
* A list view shows any number of entries while each write stays small:
|
|
22
|
-
* the plugin sends only the rows around the scroll position plus the total,
|
|
23
|
-
* and the view reports where it scrolled to. After the `scrolled` handler
|
|
24
|
-
* returns, the SDK evaluates the `bind` function again and sends the new
|
|
25
|
-
* window. Each row object and each of its fields count as one value toward
|
|
26
|
-
* `values`, and so does the array holding them.
|
|
27
30
|
* ```ts
|
|
28
|
-
* declare const entries: { title: string; duration:
|
|
29
|
-
* import type { View } from "@vestra-player/sdk";
|
|
31
|
+
* declare const entries: { id: string; title: string; duration: string }[];
|
|
30
32
|
* declare const list: View;
|
|
31
33
|
* // ---cut---
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* // 200 rows, or fewer if the limits were smaller: 2 fields per row make
|
|
35
|
-
* // 3 values (the row and its fields), plus 1 for the array.
|
|
36
|
-
* const size = Math.min(
|
|
37
|
-
* 200,
|
|
38
|
-
* UI_VALUE_LIMITS.arrayRows,
|
|
39
|
-
* Math.floor((UI_VALUE_LIMITS.values - 1) / 3),
|
|
40
|
-
* );
|
|
41
|
-
* let first = 0;
|
|
42
|
-
* list.bind(() => ({
|
|
43
|
-
* total: entries.length,
|
|
44
|
-
* rows: entries.slice(first, first + size),
|
|
45
|
-
* }));
|
|
46
|
-
* // The Slint view reports its first visible row; the window follows it.
|
|
47
|
-
* list.on("scrolled", (row: number) => {
|
|
48
|
-
* first = Math.max(0, row - Math.floor(size / 2));
|
|
49
|
-
* });
|
|
34
|
+
* list.bind(() => ({ rows: entries.map((e) => ({ ...e, selected: false })) }));
|
|
50
35
|
* ```
|
|
51
36
|
*
|
|
52
37
|
* @since 1.5.10
|
|
@@ -59,16 +44,16 @@ export declare const UI_VALUE_LIMITS: {
|
|
|
59
44
|
/**
|
|
60
45
|
* Values in all, counting every array, object, string, number, boolean
|
|
61
46
|
* and null, the value itself included; a Markdown document is one
|
|
62
|
-
* (
|
|
47
|
+
* (1,048,576; 16,384 before player 0.6.0).
|
|
63
48
|
*/
|
|
64
|
-
readonly values:
|
|
65
|
-
/** Rows of one array (
|
|
66
|
-
readonly arrayRows:
|
|
49
|
+
readonly values: number;
|
|
50
|
+
/** Rows of one array (100,000; 4,096 before player 0.6.0). */
|
|
51
|
+
readonly arrayRows: 100000;
|
|
67
52
|
/** UTF-8 bytes of one string or object field name (64 KiB). */
|
|
68
53
|
readonly stringBytes: number;
|
|
69
54
|
/**
|
|
70
55
|
* UTF-8 bytes of all strings and field names together, the text of the
|
|
71
|
-
* value's Markdown documents included (1 MiB).
|
|
56
|
+
* value's Markdown documents included (32 MiB; 1 MiB before player 0.6.0).
|
|
72
57
|
*/
|
|
73
58
|
readonly textBytes: number;
|
|
74
59
|
};
|
package/dist/plugin/values.js
CHANGED
|
@@ -10,41 +10,26 @@
|
|
|
10
10
|
* makes `Window.show` reject; a panel or settings page opens without it and
|
|
11
11
|
* the failure goes to the plugin's `onError`.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Since player 0.6.0 (SDK 1.5.46) they allow a whole list: a plugin binds
|
|
14
|
+
* all of its rows (a playlist of 50,000 entries fits) and the view draws
|
|
15
|
+
* them with a Slint `ListView`, which builds only the rows on screen. The
|
|
16
|
+
* SDK sends a changed array as row patches, and an array larger than one
|
|
17
|
+
* request (about 512 KiB of rows) as its first rows followed by `insert`
|
|
18
|
+
* patches of the rest; the player converts large writes off its interface
|
|
19
|
+
* thread. Before player 0.6.0 the limits were 4,096 rows, 16,384 values and
|
|
20
|
+
* 1 MiB of text, so a long list had to be sent as a window of rows.
|
|
21
|
+
*
|
|
22
|
+
* In a row, `null` where the view declares an `image` shows no picture
|
|
23
|
+
* (player 0.6.0): rows without a thumbnail need no placeholder image.
|
|
24
|
+
*
|
|
25
|
+
* @example Bind a whole list
|
|
26
|
+
* The view draws every row with a `ListView`; the SDK sends what changed.
|
|
17
27
|
*
|
|
18
|
-
* @example Send a long playlist as a window of rows
|
|
19
|
-
* A list view shows any number of entries while each write stays small:
|
|
20
|
-
* the plugin sends only the rows around the scroll position plus the total,
|
|
21
|
-
* and the view reports where it scrolled to. After the `scrolled` handler
|
|
22
|
-
* returns, the SDK evaluates the `bind` function again and sends the new
|
|
23
|
-
* window. Each row object and each of its fields count as one value toward
|
|
24
|
-
* `values`, and so does the array holding them.
|
|
25
28
|
* ```ts
|
|
26
|
-
* declare const entries: { title: string; duration:
|
|
27
|
-
* import type { View } from "@vestra-player/sdk";
|
|
29
|
+
* declare const entries: { id: string; title: string; duration: string }[];
|
|
28
30
|
* declare const list: View;
|
|
29
31
|
* // ---cut---
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* // 200 rows, or fewer if the limits were smaller: 2 fields per row make
|
|
33
|
-
* // 3 values (the row and its fields), plus 1 for the array.
|
|
34
|
-
* const size = Math.min(
|
|
35
|
-
* 200,
|
|
36
|
-
* UI_VALUE_LIMITS.arrayRows,
|
|
37
|
-
* Math.floor((UI_VALUE_LIMITS.values - 1) / 3),
|
|
38
|
-
* );
|
|
39
|
-
* let first = 0;
|
|
40
|
-
* list.bind(() => ({
|
|
41
|
-
* total: entries.length,
|
|
42
|
-
* rows: entries.slice(first, first + size),
|
|
43
|
-
* }));
|
|
44
|
-
* // The Slint view reports its first visible row; the window follows it.
|
|
45
|
-
* list.on("scrolled", (row: number) => {
|
|
46
|
-
* first = Math.max(0, row - Math.floor(size / 2));
|
|
47
|
-
* });
|
|
32
|
+
* list.bind(() => ({ rows: entries.map((e) => ({ ...e, selected: false })) }));
|
|
48
33
|
* ```
|
|
49
34
|
*
|
|
50
35
|
* @since 1.5.10
|
|
@@ -57,18 +42,18 @@ export const UI_VALUE_LIMITS = {
|
|
|
57
42
|
/**
|
|
58
43
|
* Values in all, counting every array, object, string, number, boolean
|
|
59
44
|
* and null, the value itself included; a Markdown document is one
|
|
60
|
-
* (
|
|
45
|
+
* (1,048,576; 16,384 before player 0.6.0).
|
|
61
46
|
*/
|
|
62
|
-
values:
|
|
63
|
-
/** Rows of one array (
|
|
64
|
-
arrayRows:
|
|
47
|
+
values: 1024 * 1024,
|
|
48
|
+
/** Rows of one array (100,000; 4,096 before player 0.6.0). */
|
|
49
|
+
arrayRows: 100_000,
|
|
65
50
|
/** UTF-8 bytes of one string or object field name (64 KiB). */
|
|
66
51
|
stringBytes: 64 * 1024,
|
|
67
52
|
/**
|
|
68
53
|
* UTF-8 bytes of all strings and field names together, the text of the
|
|
69
|
-
* value's Markdown documents included (1 MiB).
|
|
54
|
+
* value's Markdown documents included (32 MiB; 1 MiB before player 0.6.0).
|
|
70
55
|
*/
|
|
71
|
-
textBytes: 1024 * 1024,
|
|
56
|
+
textBytes: 32 * 1024 * 1024,
|
|
72
57
|
};
|
|
73
58
|
const L = UI_VALUE_LIMITS;
|
|
74
59
|
const bytes = (text) => Buffer.byteLength(text, "utf8");
|
package/dist/wire.d.ts
CHANGED
|
@@ -88,12 +88,15 @@ export type RowOp = {
|
|
|
88
88
|
export type PropertyWrite = {
|
|
89
89
|
value: Json;
|
|
90
90
|
seq?: number;
|
|
91
|
+
more?: boolean;
|
|
91
92
|
} | {
|
|
92
93
|
patch: {
|
|
93
94
|
base: number;
|
|
94
95
|
seq: number;
|
|
95
96
|
ops: RowOp[];
|
|
96
97
|
};
|
|
98
|
+
/** A part of a whole value sent in parts, not the last (player 0.6.0). */
|
|
99
|
+
more?: boolean;
|
|
97
100
|
};
|
|
98
101
|
/**
|
|
99
102
|
* A definite refusal from the player (or the plugin runner), with a stable
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.46: 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
|
|
|
@@ -2284,6 +2284,7 @@ The table lists the API whose documentation names a player version, newest last:
|
|
|
2284
2284
|
| 0.5.20 | VideoShaders |
|
|
2285
2285
|
| 0.5.21 | Scope.when, When, Bindings.onChange |
|
|
2286
2286
|
| 0.5.25 | UpscalingReason.cause, UpscalingReason.plugins, UpscalingDecodingCause, UpscalingPlugin, UpscalingVideoSize, UpscalingState.video, UpscalingState.area, UpscalingState.gpu, UpscalingState.hwdecCurrent, UpscalingState.inChain, VideoUpscaling.onChange |
|
|
2287
|
+
| 0.6.0 | PropValue |
|
|
2287
2288
|
|
|
2288
2289
|
The Slint library carries no version tags: `Theme.danger` needs player 0.3.0; `@vestra/icons.slint` and the palette `vestra-light` need 0.4.0 (Theme).
|
|
2289
2290
|
|
|
@@ -2352,7 +2353,7 @@ The check follows types, not names: `plugin.player.seek`, `const { player } = pl
|
|
|
2352
2353
|
|
|
2353
2354
|
## Reference: Limits
|
|
2354
2355
|
|
|
2355
|
-
The player bounds what one request may cost it. The limits are guards against runaway plugins, not sizes to aim for: a
|
|
2356
|
+
The player bounds what one request may cost it. The limits are guards against runaway plugins, not sizes to aim for. Since player 0.6.0 a whole list fits (50,000 playlist rows): bind it and draw it with a Slint `ListView` (a long list should use one: a `for` builds every row). The SDK sends row patches, and an array property larger than one request in parts (SDK 1.5.46); only a property's top-level array without Markdown documents is split, so any other value, and each row, must fit in one request (a row of more than 512 KiB is refused with `limit`). The SDK checks the same limits before it sends a request, so a plugin's own tests hit them too (TestHost enforces them). A name in code style is a constant you can import from `@vestra-player/sdk/testing`.
|
|
2356
2357
|
|
|
2357
2358
|
#### Commands
|
|
2358
2359
|
|
|
@@ -2380,10 +2381,10 @@ One value written to a window, panel or settings page property (View.set, View.b
|
|
|
2380
2381
|
| Limit | Value | Meaning |
|
|
2381
2382
|
| --- | --- | --- |
|
|
2382
2383
|
| `UI_VALUE_LIMITS.depth` | 8 | Nesting depth; the value itself is depth 0. |
|
|
2383
|
-
| `UI_VALUE_LIMITS.values` |
|
|
2384
|
-
| `UI_VALUE_LIMITS.arrayRows` |
|
|
2384
|
+
| `UI_VALUE_LIMITS.values` | 1048576 | Values in all, counting every array, object, string, number, boolean and null, the value itself included; a Markdown document is one. |
|
|
2385
|
+
| `UI_VALUE_LIMITS.arrayRows` | 100000 | Rows of one array. |
|
|
2385
2386
|
| `UI_VALUE_LIMITS.stringBytes` | 65536 (64 KiB) | UTF-8 bytes of one string or object field name. |
|
|
2386
|
-
| `UI_VALUE_LIMITS.textBytes` |
|
|
2387
|
+
| `UI_VALUE_LIMITS.textBytes` | 33554432 (32 MiB) | UTF-8 bytes of all strings and field names together, the text of the value's Markdown documents included. |
|
|
2387
2388
|
|
|
2388
2389
|
#### Text values
|
|
2389
2390
|
|
|
@@ -5816,6 +5817,7 @@ Every SDK release's CHANGELOG entry opens with who must change and what happens
|
|
|
5816
5817
|
|
|
5817
5818
|
| Release | Who must change | Guide |
|
|
5818
5819
|
| --- | --- | --- |
|
|
5820
|
+
| [1.5.46](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1546) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. | — |
|
|
5819
5821
|
| [1.5.45](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1545) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. | — |
|
|
5820
5822
|
| [1.5.44](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1544) | **Nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability. | — |
|
|
5821
5823
|
| [1.5.43](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md#1543) | **Nobody.** Additions only, `apiVersion` stays `vestra/1`, no new capability. | — |
|
|
@@ -7102,20 +7104,23 @@ ImageOptions.maxSide for small pictures such as logos and
|
|
|
7102
7104
|
thumbnails.
|
|
7103
7105
|
|
|
7104
7106
|
Limits per value (`UI_VALUE_LIMITS` in `@vestra-player/sdk/testing`,
|
|
7105
|
-
player 0.4.3
|
|
7106
|
-
|
|
7107
|
-
null counts, the value itself included; a
|
|
7108
|
-
|
|
7109
|
-
field
|
|
7110
|
-
document). A list of rows
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7107
|
+
player 0.6.0; players 0.4.3 to 0.5: 16384 values, 4096 rows, 1 MiB of
|
|
7108
|
+
text): nesting depth 8, 1,048,576 values in total (every array, object,
|
|
7109
|
+
string, number, boolean and null counts, the value itself included; a
|
|
7110
|
+
document or an image is one), 100,000 rows per array, 64 KiB per string
|
|
7111
|
+
or field name and 32 MiB of text in total, field names and the text of
|
|
7112
|
+
its Markdown documents included (256 KB per document). A list of rows
|
|
7113
|
+
with 13 fields is 14 values per row, so 50,000 such rows fit. An array
|
|
7114
|
+
property larger than one 1 MiB request is sent in parts (SDK 1.5.46, a
|
|
7115
|
+
player that takes row patches); only the top-level array is split, so
|
|
7116
|
+
any other value, and each row, must fit in one request (a row of more
|
|
7117
|
+
than 512 KiB is refused with `limit`). A value over a limit is refused
|
|
7118
|
+
with code
|
|
7119
|
+
`limit` and the property keeps what it showed (see View.set);
|
|
7120
|
+
`TestHost` refuses it the same way.
|
|
7121
|
+
|
|
7122
|
+
Bind a whole list and draw it with a Slint `ListView`, which builds only
|
|
7123
|
+
the rows on screen; the SDK sends what changed (row patches).
|
|
7119
7124
|
|
|
7120
7125
|
```ts
|
|
7121
7126
|
type PropValue = PropData | ImageRef;
|
|
@@ -17854,53 +17859,41 @@ keeps what it showed) and reported to `Scope.onError`: a write by
|
|
|
17854
17859
|
makes `Window.show` reject; a panel or settings page opens without it and
|
|
17855
17860
|
the failure goes to the plugin's `onError`.
|
|
17856
17861
|
|
|
17857
|
-
|
|
17858
|
-
|
|
17859
|
-
|
|
17860
|
-
|
|
17862
|
+
Since player 0.6.0 (SDK 1.5.46) they allow a whole list: a plugin binds
|
|
17863
|
+
all of its rows (a playlist of 50,000 entries fits) and the view draws
|
|
17864
|
+
them with a Slint `ListView`, which builds only the rows on screen. The
|
|
17865
|
+
SDK sends a changed array as row patches, and an array larger than one
|
|
17866
|
+
request (about 512 KiB of rows) as its first rows followed by `insert`
|
|
17867
|
+
patches of the rest; the player converts large writes off its interface
|
|
17868
|
+
thread. Before player 0.6.0 the limits were 4,096 rows, 16,384 values and
|
|
17869
|
+
1 MiB of text, so a long list had to be sent as a window of rows.
|
|
17870
|
+
|
|
17871
|
+
In a row, `null` where the view declares an `image` shows no picture
|
|
17872
|
+
(player 0.6.0): rows without a thumbnail need no placeholder image.
|
|
17861
17873
|
|
|
17862
17874
|
```ts
|
|
17863
17875
|
const UI_VALUE_LIMITS: {
|
|
17864
17876
|
/** Nesting depth; the value itself is depth 0 (8). */
|
|
17865
17877
|
readonly depth: 8;
|
|
17866
|
-
/**
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
readonly
|
|
17878
|
+
/**
|
|
17879
|
+
* Values in all, counting every array, object, string, number, boolean and null, the value itself included; a Markdown document is one (1,048,576; 16,384 before player 0.6.0).
|
|
17880
|
+
*/
|
|
17881
|
+
readonly values: number;
|
|
17882
|
+
/** Rows of one array (100,000; 4,096 before player 0.6.0). */
|
|
17883
|
+
readonly arrayRows: 100000;
|
|
17870
17884
|
/** UTF-8 bytes of one string or object field name (64 KiB). */
|
|
17871
17885
|
readonly stringBytes: number;
|
|
17872
|
-
/** UTF-8 bytes of all strings and field names together, the text of the value's Markdown documents included (1 MiB). */
|
|
17886
|
+
/** UTF-8 bytes of all strings and field names together, the text of the value's Markdown documents included (32 MiB; 1 MiB before player 0.6.0). */
|
|
17873
17887
|
readonly textBytes: number;
|
|
17874
17888
|
};
|
|
17875
17889
|
```
|
|
17876
17890
|
|
|
17877
|
-
Example:
|
|
17891
|
+
Example: Bind a whole list
|
|
17878
17892
|
|
|
17879
|
-
|
|
17880
|
-
the plugin sends only the rows around the scroll position plus the total,
|
|
17881
|
-
and the view reports where it scrolled to. After the `scrolled` handler
|
|
17882
|
-
returns, the SDK evaluates the `bind` function again and sends the new
|
|
17883
|
-
window. Each row object and each of its fields count as one value toward
|
|
17884
|
-
`values`, and so does the array holding them.
|
|
17885
|
-
```ts
|
|
17886
|
-
import { UI_VALUE_LIMITS } from "@vestra-player/sdk/testing";
|
|
17893
|
+
The view draws every row with a `ListView`; the SDK sends what changed.
|
|
17887
17894
|
|
|
17888
|
-
|
|
17889
|
-
|
|
17890
|
-
const size = Math.min(
|
|
17891
|
-
200,
|
|
17892
|
-
UI_VALUE_LIMITS.arrayRows,
|
|
17893
|
-
Math.floor((UI_VALUE_LIMITS.values - 1) / 3),
|
|
17894
|
-
);
|
|
17895
|
-
let first = 0;
|
|
17896
|
-
list.bind(() => ({
|
|
17897
|
-
total: entries.length,
|
|
17898
|
-
rows: entries.slice(first, first + size),
|
|
17899
|
-
}));
|
|
17900
|
-
// The Slint view reports its first visible row; the window follows it.
|
|
17901
|
-
list.on("scrolled", (row: number) => {
|
|
17902
|
-
first = Math.max(0, row - Math.floor(size / 2));
|
|
17903
|
-
});
|
|
17895
|
+
```ts
|
|
17896
|
+
list.bind(() => ({ rows: entries.map((e) => ({ ...e, selected: false })) }));
|
|
17904
17897
|
```
|
|
17905
17898
|
|
|
17906
17899
|
### IMAGE_LIMITS
|
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.46: 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 (the player's built-in plugins share one). Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n>: a line of llms-full.txt (next to this file), which has every page, declaration and example.
|
|
4
4
|
|
|
5
5
|
**Rules**
|
|
6
6
|
|
|
@@ -33,7 +33,7 @@ my-plugin/
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2218–
|
|
36
|
+
Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2218–2292.
|
|
37
37
|
|
|
38
38
|
**main.ts**
|
|
39
39
|
|
|
@@ -78,9 +78,9 @@ export default async function activate(plugin: Plugin) {
|
|
|
78
78
|
- `secrets`: Secrets.get/set/delete
|
|
79
79
|
- `rendering.manage`: Rendering.setPreference/restartPlayer
|
|
80
80
|
- `video.upscaling`: VideoUpscaling.state/set/onChange
|
|
81
|
-
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme, packages.list and rendering.state. Details:
|
|
81
|
+
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme, packages.list and rendering.state. Details: L2293–2353.
|
|
82
82
|
|
|
83
|
-
**Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`, `host` an in-memory player (TestHost): act (`host.command(id).run()`, `host.press(key)`), `await host.settle()`, assert on `host.osd`, `host.window(id).props`… Guide: L1861–2021; TestHost:
|
|
83
|
+
**Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`, `host` an in-memory player (TestHost): act (`host.command(id).run()`, `host.press(key)`), `await host.settle()`, assert on `host.osd`, `host.window(id).props`… Guide: L1861–2021; TestHost: L15484.
|
|
84
84
|
|
|
85
85
|
**Command line**
|
|
86
86
|
|
|
@@ -108,40 +108,40 @@ export default async function activate(plugin: Plugin) {
|
|
|
108
108
|
- `limit`: A player-wide limit was reached, a property value is over the limits of PropValue
|
|
109
109
|
- `reserved`: The id is `vestra` or starts with `vestra.`
|
|
110
110
|
- `versionMismatch`: The package needs a newer player
|
|
111
|
-
- Others: PluginError, llms-full.txt
|
|
111
|
+
- Others: PluginError, llms-full.txt L5954.
|
|
112
112
|
|
|
113
113
|
## Reference
|
|
114
114
|
|
|
115
115
|
- [llms-full.txt](https://vestrasys.github.io/vestra-player-sdk/llms-full.txt), also node_modules/@vestra-player/sdk/llms-full.txt; pages: [site](https://vestrasys.github.io/vestra-player-sdk/).
|
|
116
116
|
- Tutorials: Getting started L9–134; Add a window L135–213; Add settings and translations L214–367; Test, check, pack and publish L368–424; Write your own blur kernel L425–685
|
|
117
117
|
- How-to: Control playback L686–854; Store a plugin's data L855–961; Load subtitles from a plugin L962–1074; Keep a slider responsive while dragging L1075–1262; Remember a window's state between runs L1263–1414; Offer a GPU rendering switch L1415–1540; Keep a command's state without keeping the plugin running L1541–1697; Translate a plugin L1698–1860; Test a plugin L1861–2021; Publish a plugin L2022–2123; Troubleshooting L2124–2217
|
|
118
|
-
- Reference: Manifest L2218–
|
|
119
|
-
- Explanation: How a plugin runs
|
|
120
|
-
- Guidelines: Plugin rules
|
|
121
|
-
- Upgrading: Migrate a plugin to English source text
|
|
122
|
-
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html):
|
|
123
|
-
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html):
|
|
118
|
+
- Reference: Manifest L2218–2292; Capabilities L2293–2353; Limits L2354–2499; Commands, menus and bindings L2500–3025; Platforms L3026–3056; Theme L3057–3406; Command line L3407–3464; Error codes L3465–3512; Translations L3513–3605; Test host L3606–3792
|
|
119
|
+
- Explanation: How a plugin runs L3793–3995; Commands, menus and key bindings L3996–4150; Playback state, controls and sources L4151–4310; Video and audio processing L4311–4431; Windows, panels, settings pages, dialogs and the OSD L4432–4694; GPU and software rendering L4695–4711; The player's Rust core L4712–5053; How the player runs and talks to plugins L5054–5393
|
|
120
|
+
- Guidelines: Plugin rules L5394–5405; Interface guidelines L5406–5522; Menu guidelines L5523–5581
|
|
121
|
+
- Upgrading: Migrate a plugin to English source text L5582–5731; Migrating off package rollback (1.5.16, removed in 1.5.17) L5732–5809; Who must change, release by release L5810–5876
|
|
122
|
+
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L5877–15157
|
|
123
|
+
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L15158–18020
|
|
124
124
|
|
|
125
125
|
## API
|
|
126
126
|
|
|
127
127
|
Where each `plugin.<module>` is declared:
|
|
128
128
|
|
|
129
|
-
- Plugin
|
|
130
|
-
- Commands
|
|
129
|
+
- Plugin L6293, Scope L6029, PluginError L5954, Disposable L5892
|
|
130
|
+
- Commands L6883; When L6480; Windows L7829; Panels L9275; Dialogs L9945; Player L10686; video: VideoFilter L11566, VideoView L11666, RenderOptions L11753, VideoShaders L11831, VideoUpscaling L12189; audio: AudioFilter L11611; Subtitles L12425; media: MediaCapture L12829; Resolvers L13059; Packages L13461; Osd L11431; Host L11273; Clipboard L13790; Power L13877; Tools L13936; Storage L14088; System L14287; Secrets L14367; window: MainWindow L9592; App L9772; Rendering L14630; Bindings L13689; Ui L7175; Timeline L9504; Drops L8522; Appearance L8603; I18n L8836; Settings L9119.
|
|
131
131
|
|
|
132
132
|
## Examples
|
|
133
133
|
|
|
134
134
|
Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
|
|
135
135
|
|
|
136
|
-
- hello: The README Quick start: one menu command and an on-screen message.
|
|
137
|
-
- blur: A tool window with one slider that blurs the playing video.
|
|
138
|
-
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone.
|
|
139
|
-
- bookmark: A tool window bound to the playback state: remember the position and jump back to it.
|
|
140
|
-
- panel-menu: A sidebar panel with its own right-click menu.
|
|
141
|
-
- menus: Radio menu items, separator groups and a menu item whose title changes.
|
|
142
|
-
- theme-widgets: A tool window drawn with the player's theme and widgets.
|
|
143
|
-
- markdown: A window that shows a Markdown document.
|
|
144
|
-
- i18n: A plugin in the player's language: English source text, a Chinese translation.
|
|
136
|
+
- hello: The README Quick start: one menu command and an on-screen message. L18455–18550
|
|
137
|
+
- blur: A tool window with one slider that blurs the playing video. L18551–18818
|
|
138
|
+
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L18819–19157
|
|
139
|
+
- bookmark: A tool window bound to the playback state: remember the position and jump back to it. L19158–19350
|
|
140
|
+
- panel-menu: A sidebar panel with its own right-click menu. L19351–19565
|
|
141
|
+
- menus: Radio menu items, separator groups and a menu item whose title changes. L19566–19731
|
|
142
|
+
- theme-widgets: A tool window drawn with the player's theme and widgets. L19732–19901
|
|
143
|
+
- markdown: A window that shows a Markdown document. L19902–20104
|
|
144
|
+
- i18n: A plugin in the player's language: English source text, a Chinese translation. L20105–20278
|
|
145
145
|
|
|
146
146
|
## Optional
|
|
147
147
|
|