@vestra-player/sdk 1.5.45 → 1.5.47
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 +23 -0
- package/bin/vestra-plugin.mjs +59 -1
- package/dist/commands.js +5 -4
- package/dist/plugin/bindings.d.ts +5 -2
- package/dist/plugin/bindings.js +5 -2
- package/dist/plugin/patch.d.ts +52 -1
- package/dist/plugin/patch.js +104 -6
- package/dist/plugin/testing.js +63 -6
- package/dist/plugin/types.d.ts +24 -18
- package/dist/plugin/ui.d.ts +8 -2
- package/dist/plugin/ui.js +279 -19
- 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 +72 -70
- package/llms.txt +22 -22
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.47
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** No changes to the TypeScript API, `apiVersion` stays `vestra/1`, no new capability, no player version needed. The built-in command table follows player 0.6.0's new default keys: a plugin whose documentation or tests list the player's default keys (from `BUILTIN_COMMANDS`, or Escape, Enter, F11 and the side mouse buttons by hand) should update them.
|
|
6
|
+
|
|
7
|
+
- **Player 0.6.0 default keys** (`BUILTIN_COMMANDS`, the generated key tables, `HostAction`): **Toggle Fullscreen** (`host/fullscreen`) is `Escape` only (up to 0.5.x `Enter`, `Alt+Enter`, `F11`); **Leave Fullscreen** (`host/exit-fullscreen`) has no default key (up to 0.5.x `Escape`); the new **Enter Fullscreen** (`host/enter-fullscreen`) has no default key and no menu row and is enabled only when windowed (Leave Fullscreen only in fullscreen); **Previous** is `PageUp`, `Mouse:X2` and **Next** `PageDown`, `Mouse:X1` (the side buttons swapped: back is next, forward is previous). The side buttons now fire anywhere in the main window or the floating sidebar window, except while a menu is open, not only over the video. `host.run` takes the same actions as before (`fullscreen` toggles). `TestHost`'s core actions use the new Previous / Next bindings.
|
|
8
|
+
- **Rows that cannot change are not encoded again.** An update pass encodes every row of a bound array to find the changed ones; in a 20,000-row list that was about 40% of a click. A deep-frozen row (`Object.freeze` on it and on everything inside it: plain objects and arrays, no getters, no own `toJSON`; image and document handles allowed: they are immutable, their wire form is fixed when made, so they need no freezing) that the bind function returns again is now encoded once more and then taken from a cache kept by the row object (weakly: a row the plugin drops goes with its entry), with its limit counts. Rows that are not frozen, or hold a mutable part, are encoded on every pass as before, so changes made in place are still sent. To benefit, keep an unchanged row as the same frozen object and make a new one when it changes ([Arrays: only the changed rows are sent](docs/explanation/ui.md#arrays-only-the-changed-rows-are-sent)). Measured in the test host with the playlist at 20,000 rows memoized that way: a click from about 24 ms to about 14 ms to the rows write.
|
|
9
|
+
- _Commands, menus and key bindings_: the example of a disabled command letting its key go no longer uses Escape on Leave Fullscreen.
|
|
10
|
+
|
|
11
|
+
## 1.5.46
|
|
12
|
+
|
|
13
|
+
**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"`.**
|
|
14
|
+
|
|
15
|
+
- **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.
|
|
16
|
+
- **Writes larger than one request are split.** The protocol line stays 1 MiB (`MESSAGE_LIMIT`). With a player that takes row patches:
|
|
17
|
+
- 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);
|
|
18
|
+
- 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;
|
|
19
|
+
- 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;
|
|
20
|
+
- 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;
|
|
21
|
+
- 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).
|
|
22
|
+
- **`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.
|
|
23
|
+
- **`null` for an image in a row** shows no picture (player 0.6.0), so rows without a thumbnail need no placeholder image.
|
|
24
|
+
- **`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.
|
|
25
|
+
|
|
3
26
|
## 1.5.45
|
|
4
27
|
|
|
5
28
|
**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/commands.js
CHANGED
|
@@ -265,8 +265,8 @@ vestra.playback | copy-name | Copy to Clipboard ▸ File Name
|
|
|
265
265
|
vestra.playback | copy-folder | Copy to Clipboard ▸ Folder Path
|
|
266
266
|
vestra.playback | copy-path | Copy to Clipboard ▸ Full Path
|
|
267
267
|
host | play-pause | Playback ▸ Play/Pause | Space, Mouse:LeftClick
|
|
268
|
-
host | previous | Playback ▸ Previous | PageUp, Mouse:
|
|
269
|
-
host | next | Playback ▸ Next | PageDown, Mouse:
|
|
268
|
+
host | previous | Playback ▸ Previous | PageUp, Mouse:X2
|
|
269
|
+
host | next | Playback ▸ Next | PageDown, Mouse:X1
|
|
270
270
|
host | frame-back | Playback ▸ Seek ▸ Previous Frame | D
|
|
271
271
|
host | frame-forward | Playback ▸ Seek ▸ Next Frame | F
|
|
272
272
|
vestra.playback | keyframe-back | Playback ▸ Seek ▸ Previous Keyframe | Ctrl+Shift+ArrowLeft
|
|
@@ -492,7 +492,7 @@ vestra.audio | audio.normalize | Audio ▸ Processing ▸ Normalize | Shift+N
|
|
|
492
492
|
vestra.audio | audio.clarity | Audio ▸ Processing ▸ Crystalizer | Shift+C
|
|
493
493
|
vestra.audio | audio.reset | Audio ▸ Reset
|
|
494
494
|
vestra.visualizer | audio-cover | Audio ▸ Show Album Cover for Audio Only
|
|
495
|
-
host | fullscreen | Toggle Fullscreen |
|
|
495
|
+
host | fullscreen | Toggle Fullscreen | Escape
|
|
496
496
|
vestra.window | pip | Screen ▸ Picture-in-Picture
|
|
497
497
|
vestra.window | mini | Screen ▸ Mini Size | \`
|
|
498
498
|
vestra.window | size.0.5 | Screen ▸ 0.5× | 1
|
|
@@ -536,7 +536,8 @@ vestra.preferences | language.fr | Settings ▸ Language ▸ Français
|
|
|
536
536
|
vestra.preferences | language.es | Settings ▸ Language ▸ Español
|
|
537
537
|
vestra.info | info.about | About Vestra Player… | F1
|
|
538
538
|
host | quit | Quit | Cmd+Q
|
|
539
|
-
host |
|
|
539
|
+
host | enter-fullscreen | (Enter Fullscreen)
|
|
540
|
+
host | exit-fullscreen | (Leave Fullscreen)
|
|
540
541
|
host | sidebar | (Toggle Sidebar)
|
|
541
542
|
vestra.info | info.brief | (Brief Info) | Shift+Tab
|
|
542
543
|
vestra.playlist | playlist.play | (Play) | Enter, P | panel
|
|
@@ -66,8 +66,11 @@ export declare const BINDING_INPUTS: Readonly<{
|
|
|
66
66
|
*/
|
|
67
67
|
readonly named: readonly ["Space", "Enter", "Tab", "Escape", "Backspace", "Delete", "Insert", "Home", "End", "PageUp", "PageDown", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Plus", ...string[], "NumpadPlus", "NumpadMinus", "NumpadMultiply", "NumpadDivide", "NumpadEnter", "NumpadDecimal"];
|
|
68
68
|
/**
|
|
69
|
-
* Mouse inputs on the video area
|
|
70
|
-
*
|
|
69
|
+
* Mouse inputs on the video area; the side buttons `Mouse:X1` (back) and
|
|
70
|
+
* `Mouse:X2` (forward) anywhere in the main window or the floating sidebar
|
|
71
|
+
* window, except while a menu is open (player 0.6.0).
|
|
72
|
+
* `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a
|
|
73
|
+
* double click.
|
|
71
74
|
*/
|
|
72
75
|
readonly mouse: readonly ["Mouse:LeftClick", "Mouse:LeftDoubleClick", "Mouse:LeftDoubleClick:LeftHalf", "Mouse:LeftDoubleClick:RightHalf", "Mouse:MiddleClick", "Mouse:X1", "Mouse:X2", "Mouse:WheelUp", "Mouse:WheelDown", "Mouse:WheelLeft", "Mouse:WheelRight"];
|
|
73
76
|
}>;
|
package/dist/plugin/bindings.js
CHANGED
|
@@ -97,8 +97,11 @@ export const BINDING_INPUTS = Object.freeze({
|
|
|
97
97
|
"NumpadDecimal",
|
|
98
98
|
],
|
|
99
99
|
/**
|
|
100
|
-
* Mouse inputs on the video area
|
|
101
|
-
*
|
|
100
|
+
* Mouse inputs on the video area; the side buttons `Mouse:X1` (back) and
|
|
101
|
+
* `Mouse:X2` (forward) anywhere in the main window or the floating sidebar
|
|
102
|
+
* window, except while a menu is open (player 0.6.0).
|
|
103
|
+
* `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a
|
|
104
|
+
* double click.
|
|
102
105
|
*/
|
|
103
106
|
mouse: [
|
|
104
107
|
"Mouse:LeftClick",
|
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 });
|
|
@@ -2211,7 +2254,7 @@ export class TestHost {
|
|
|
2211
2254
|
["Playback", "Previous"],
|
|
2212
2255
|
202,
|
|
2213
2256
|
"domain",
|
|
2214
|
-
["PageUp", "Mouse:
|
|
2257
|
+
["PageUp", "Mouse:X2"],
|
|
2215
2258
|
false,
|
|
2216
2259
|
],
|
|
2217
2260
|
[
|
|
@@ -2219,7 +2262,7 @@ export class TestHost {
|
|
|
2219
2262
|
["Playback", "Next"],
|
|
2220
2263
|
203,
|
|
2221
2264
|
"domain",
|
|
2222
|
-
["PageDown", "Mouse:
|
|
2265
|
+
["PageDown", "Mouse:X1"],
|
|
2223
2266
|
false,
|
|
2224
2267
|
],
|
|
2225
2268
|
[
|
|
@@ -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));
|
|
@@ -4202,7 +4248,7 @@ export class TestHost {
|
|
|
4202
4248
|
throw new HostError("invalid", "seconds must be 0.1..3600 in tenths");
|
|
4203
4249
|
seconds = p.seconds;
|
|
4204
4250
|
}
|
|
4205
|
-
// SDK 1.5.21: the main window's Toggle Fullscreen, like
|
|
4251
|
+
// SDK 1.5.21: the main window's Toggle Fullscreen, like Escape: in when
|
|
4206
4252
|
// windowed, out when fullscreen, then `window.state`.
|
|
4207
4253
|
if (p.action === "fullscreen") {
|
|
4208
4254
|
this.coreRuns.push("fullscreen");
|
|
@@ -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
|
@@ -999,9 +999,12 @@ export interface CommandOptions {
|
|
|
999
999
|
* `Home`, `End`, `PageUp`, `PageDown`, `ArrowLeft`/`Right`/`Up`/`Down`,
|
|
1000
1000
|
* `Plus`, any other single symbol as typed (`">"`, not `"Shift+."`),
|
|
1001
1001
|
* `Numpad0`–`Numpad9`, `NumpadPlus`, `NumpadMinus`, `NumpadMultiply`,
|
|
1002
|
-
* `NumpadDivide`, `NumpadEnter`, `NumpadDecimal`,
|
|
1002
|
+
* `NumpadDivide`, `NumpadEnter`, `NumpadDecimal`, the video-area mouse
|
|
1003
1003
|
* inputs `Mouse:LeftClick`, `Mouse:LeftDoubleClick[:LeftHalf|:RightHalf]`,
|
|
1004
|
-
* `Mouse:MiddleClick`, `Mouse:
|
|
1004
|
+
* `Mouse:MiddleClick`, `Mouse:WheelUp`/`Down`/`Left`/`Right`, and the side
|
|
1005
|
+
* buttons `Mouse:X1` (back), `Mouse:X2` (forward), from 0.6.0 on anywhere
|
|
1006
|
+
* in the main window or the floating sidebar window, except while a menu
|
|
1007
|
+
* is open.
|
|
1005
1008
|
*
|
|
1006
1009
|
* Platforms: on macOS `Ctrl+X` fires for both ⌃X and ⌘X while `Cmd+X`
|
|
1007
1010
|
* means ⌘X only; on Windows `Cmd` is the Windows logo key, so write
|
|
@@ -1384,20 +1387,23 @@ export interface MarkdownOptions {
|
|
|
1384
1387
|
* thumbnails.
|
|
1385
1388
|
*
|
|
1386
1389
|
* 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
|
-
*
|
|
1390
|
+
* player 0.6.0; players 0.4.3 to 0.5: 16384 values, 4096 rows, 1 MiB of
|
|
1391
|
+
* text): nesting depth 8, 1,048,576 values in total (every array, object,
|
|
1392
|
+
* string, number, boolean and null counts, the value itself included; a
|
|
1393
|
+
* document or an image is one), 100,000 rows per array, 64 KiB per string
|
|
1394
|
+
* or field name and 32 MiB of text in total, field names and the text of
|
|
1395
|
+
* its Markdown documents included (256 KB per document). A list of rows
|
|
1396
|
+
* with 13 fields is 14 values per row, so 50,000 such rows fit. An array
|
|
1397
|
+
* property larger than one 1 MiB request is sent in parts (SDK 1.5.46, a
|
|
1398
|
+
* player that takes row patches); only the top-level array is split, so
|
|
1399
|
+
* any other value, and each row, must fit in one request (a row of more
|
|
1400
|
+
* than 512 KiB is refused with `limit`). A value over a limit is refused
|
|
1401
|
+
* with code
|
|
1402
|
+
* `limit` and the property keeps what it showed (see {@link View.set});
|
|
1403
|
+
* `TestHost` refuses it the same way.
|
|
1404
|
+
*
|
|
1405
|
+
* Bind a whole list and draw it with a Slint `ListView`, which builds only
|
|
1406
|
+
* the rows on screen; the SDK sends what changed (row patches).
|
|
1401
1407
|
*
|
|
1402
1408
|
* @since 1.0.0
|
|
1403
1409
|
* @category UI
|
|
@@ -6058,13 +6064,13 @@ export interface Player {
|
|
|
6058
6064
|
* | --- | --- | --- |
|
|
6059
6065
|
* | `stop` | F4 | close the source |
|
|
6060
6066
|
* | `play-pause` | Space, a left click on the video (player 0.5.2) | pause / resume, replay after the end |
|
|
6061
|
-
* | `previous` / `next` | PageUp, Mouse:
|
|
6067
|
+
* | `previous` / `next` | PageUp, Mouse:X2 / PageDown, Mouse:X1 (from 0.6.0; up to 0.5.x X1 was previous and X2 next) | ask the sequence provider |
|
|
6062
6068
|
* | `frame-back` / `frame-forward` | D / F | `player.step("frame", …)` |
|
|
6063
6069
|
* | `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds (or `host.run`'s `seconds`), exact or to keyframes as `setStepSeek` says |
|
|
6064
6070
|
* | `rate-normal`, `rate-down`, `rate-up` | Z, X, C | 1×, ±0.1 within 0.25–4 |
|
|
6065
6071
|
* | `volume-up` / `volume-down` | ↑ / ↓, wheel | ±5 (`volume-up` stops at 100) |
|
|
6066
6072
|
* | `mute` | M | toggle mute |
|
|
6067
|
-
* | `fullscreen` | Enter, Alt+Enter, F11 | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
|
|
6073
|
+
* | `fullscreen` | Escape (from 0.6.0; up to 0.5.x Enter, Alt+Enter, F11) | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
|
|
6068
6074
|
*
|
|
6069
6075
|
* In the binding overlay their keys are `host/<action>`. `fullscreen`
|
|
6070
6076
|
* changes no playback state: read the result from `window.state.fullscreen`
|
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;
|