@vestra-player/sdk 1.5.43 → 1.5.45
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 +21 -1
- package/dist/player-runtime.d.ts +1 -0
- package/dist/player-runtime.js +1 -0
- package/dist/plugin/index.js +4 -1
- package/dist/plugin/patch.d.ts +101 -0
- package/dist/plugin/patch.js +337 -0
- package/dist/plugin/player.d.ts +11 -0
- package/dist/plugin/player.js +80 -7
- package/dist/plugin/runtime.d.ts +64 -1
- package/dist/plugin/runtime.js +227 -16
- package/dist/plugin/scope.d.ts +9 -1
- package/dist/plugin/scope.js +24 -0
- package/dist/plugin/settings.js +2 -2
- package/dist/plugin/testing.d.ts +7 -0
- package/dist/plugin/testing.js +139 -14
- package/dist/plugin/types.d.ts +24 -15
- package/dist/plugin/ui.d.ts +7 -3
- package/dist/plugin/ui.js +236 -22
- package/dist/wire.d.ts +42 -3
- package/dist/wire.js +3 -3
- package/llms-full.txt +148 -26
- package/llms.txt +23 -23
- package/package.json +1 -1
- package/slint/vestra/markdown.slint +17 -11
- package/slint/vestra/widgets.slint +770 -1
package/dist/plugin/testing.js
CHANGED
|
@@ -86,6 +86,7 @@ import { nodeProblem, VIDEO_PROCESSING_LIMITS } from "../processing.js";
|
|
|
86
86
|
import { shaderIdProblem, shaderListProblem } from "./shaders.js";
|
|
87
87
|
import { RENDER_DEFAULTS, renderProblem, VIEW_DEFAULTS, viewProblem, } from "./view.js";
|
|
88
88
|
import { createPlugin } from "./index.js";
|
|
89
|
+
import { applyOps, PATCH_FEATURE, PATCH_OPS } from "./patch.js";
|
|
89
90
|
import { evaluate } from "./when.js";
|
|
90
91
|
import { WAKE_EVENTS } from "./runtime.js";
|
|
91
92
|
import { THEMES } from "./appearance.js";
|
|
@@ -929,6 +930,54 @@ export class TestHost {
|
|
|
929
930
|
if (reason !== null && reason !== undefined)
|
|
930
931
|
this.#declared(reason, "reason");
|
|
931
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* Row patches (player 0.6.0): the `seq` of each property's last write, by
|
|
935
|
+
* view and property (none: 0, the initial properties).
|
|
936
|
+
*/
|
|
937
|
+
#writes = new Map();
|
|
938
|
+
#resetWrites(view) {
|
|
939
|
+
for (const slot of [...this.#writes.keys()])
|
|
940
|
+
if (slot.startsWith(`${view}\u0000`))
|
|
941
|
+
this.#writes.delete(slot);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* A property write as the player takes it, returning the value the
|
|
945
|
+
* property shows after it: a whole `value`, or a `patch` of the array
|
|
946
|
+
* `current` applying on the write numbered `base` (code `stale`
|
|
947
|
+
* otherwise). The result is checked against the value limits whole: the
|
|
948
|
+
* player counts the rows a patch keeps too.
|
|
949
|
+
*/
|
|
950
|
+
#write(view, property, p, current) {
|
|
951
|
+
const slot = `${view}\u0000${property}`;
|
|
952
|
+
const patch = p.patch;
|
|
953
|
+
if (patch === undefined) {
|
|
954
|
+
const value = this.#shownValues(p.value);
|
|
955
|
+
this.#checkValues({ [property]: value });
|
|
956
|
+
this.#seeImages([value]);
|
|
957
|
+
this.#writes.set(slot, typeof p.seq === "number" ? p.seq : 0);
|
|
958
|
+
return value;
|
|
959
|
+
}
|
|
960
|
+
const { base, seq } = patch;
|
|
961
|
+
if (!Number.isInteger(base) ||
|
|
962
|
+
!Number.isInteger(seq) ||
|
|
963
|
+
base < 0 ||
|
|
964
|
+
seq < 1)
|
|
965
|
+
throw new HostError("invalid", "patch.base and patch.seq require non-negative integers (seq ≥ 1)");
|
|
966
|
+
const stale = () => new HostError("stale", "the property changed since the write this patch applies on; send the whole value");
|
|
967
|
+
if ((this.#writes.get(slot) ?? 0) !== base || !Array.isArray(current))
|
|
968
|
+
throw stale();
|
|
969
|
+
const ops = patchOps(patch.ops, current.length);
|
|
970
|
+
const resolved = ops.map((op) => op.op === "set"
|
|
971
|
+
? { ...op, row: this.#shownValues(op.row) }
|
|
972
|
+
: op.op === "insert"
|
|
973
|
+
? { ...op, rows: op.rows.map((row) => this.#shownValues(row)) }
|
|
974
|
+
: op);
|
|
975
|
+
const value = applyOps(current, resolved);
|
|
976
|
+
this.#checkValues({ [property]: value });
|
|
977
|
+
this.#seeImages(resolved.flatMap((op) => op.op === "set" ? [op.row] : op.op === "insert" ? op.rows : []));
|
|
978
|
+
this.#writes.set(slot, seq);
|
|
979
|
+
return value;
|
|
980
|
+
}
|
|
932
981
|
/**
|
|
933
982
|
* Refuse property values over the player's limits ({@link UI_VALUE_LIMITS})
|
|
934
983
|
* with `limit`, naming the property, as the player does.
|
|
@@ -3164,6 +3213,13 @@ export class TestHost {
|
|
|
3164
3213
|
* for: on a loaded machine it can finish after `settle()` returns. Poll
|
|
3165
3214
|
* the result you assert on instead (second example).
|
|
3166
3215
|
*
|
|
3216
|
+
* `settle()` does not wait for the SDK's once-a-second full update pass
|
|
3217
|
+
* (SDK 1.5.45). After `setSource` (or another player state change) only
|
|
3218
|
+
* the bindings that read a changed field of `plugin.player.state` run
|
|
3219
|
+
* again; state the plugin changed outside a handler without
|
|
3220
|
+
* `plugin.update()` shows only in the next full pass. Have the plugin
|
|
3221
|
+
* call `plugin.update()` after such a change, or wait `settle(1000)`.
|
|
3222
|
+
*
|
|
3167
3223
|
* @param ms - Extra time to wait, for debounces and timers (default 0).
|
|
3168
3224
|
*
|
|
3169
3225
|
* @example Wait past a debounced save
|
|
@@ -3816,6 +3872,7 @@ export class TestHost {
|
|
|
3816
3872
|
const id = String(p.id);
|
|
3817
3873
|
if (this.windows.has(id))
|
|
3818
3874
|
throw new HostError("conflict", "view already exists");
|
|
3875
|
+
this.#resetWrites(`w:${id}`);
|
|
3819
3876
|
const properties = this.#shownValues(p.properties);
|
|
3820
3877
|
this.#checkValues(properties);
|
|
3821
3878
|
this.#seeImages(properties);
|
|
@@ -3841,10 +3898,8 @@ export class TestHost {
|
|
|
3841
3898
|
const window = this.windows.get(String(p.id));
|
|
3842
3899
|
if (!window)
|
|
3843
3900
|
throw new HostError("missing", "view not found");
|
|
3844
|
-
const
|
|
3845
|
-
this.#
|
|
3846
|
-
this.#seeImages([value]);
|
|
3847
|
-
window.props[String(p.property)] = value;
|
|
3901
|
+
const property = String(p.property);
|
|
3902
|
+
window.props[property] = this.#write(`w:${String(p.id)}`, property, p, window.props[property]);
|
|
3848
3903
|
},
|
|
3849
3904
|
"ui.show": (p) => {
|
|
3850
3905
|
const window = this.windows.get(String(p.id));
|
|
@@ -3878,6 +3933,7 @@ export class TestHost {
|
|
|
3878
3933
|
]));
|
|
3879
3934
|
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
3880
3935
|
this.#seeImages(properties);
|
|
3936
|
+
this.#resetWrites("$mount");
|
|
3881
3937
|
const host = this;
|
|
3882
3938
|
this.panel = {
|
|
3883
3939
|
slot: String(p.slot),
|
|
@@ -4202,11 +4258,9 @@ export class TestHost {
|
|
|
4202
4258
|
this.emit("sequence.step", { direction });
|
|
4203
4259
|
},
|
|
4204
4260
|
"ui.setMount": (p) => {
|
|
4205
|
-
this.mounted();
|
|
4206
|
-
const
|
|
4207
|
-
this.#
|
|
4208
|
-
this.#seeImages([value]);
|
|
4209
|
-
this.mounted().props[String(p.property)] = value;
|
|
4261
|
+
const panel = this.mounted();
|
|
4262
|
+
const property = String(p.property);
|
|
4263
|
+
panel.props[property] = this.#write("$mount", property, p, panel.props[property]);
|
|
4210
4264
|
},
|
|
4211
4265
|
"timeline.marks": () => ({ marks: this.timeline.marks }),
|
|
4212
4266
|
"drops.provide": (p) => {
|
|
@@ -4245,6 +4299,7 @@ export class TestHost {
|
|
|
4245
4299
|
throw new HostError("invalid", "a plugin has at most 8 settings pages");
|
|
4246
4300
|
const properties = this.#initialValues(this.#shownValues(p.properties));
|
|
4247
4301
|
this.#seeImages(properties);
|
|
4302
|
+
this.#resetWrites(`$settings:${id}`);
|
|
4248
4303
|
const host = this;
|
|
4249
4304
|
this.settings.pages.set(id, {
|
|
4250
4305
|
category: String(p.category),
|
|
@@ -4267,10 +4322,8 @@ export class TestHost {
|
|
|
4267
4322
|
const page = this.settings.pages.get(String(p.id));
|
|
4268
4323
|
if (!page)
|
|
4269
4324
|
throw new HostError("missing", "page not registered");
|
|
4270
|
-
const
|
|
4271
|
-
this.#
|
|
4272
|
-
this.#seeImages([value]);
|
|
4273
|
-
page.props[String(p.property)] = value;
|
|
4325
|
+
const property = String(p.property);
|
|
4326
|
+
page.props[property] = this.#write(`$settings:${String(p.id)}`, property, p, page.props[property]);
|
|
4274
4327
|
},
|
|
4275
4328
|
"settings.unregister": (p) => {
|
|
4276
4329
|
this.settings.pages.delete(String(p.id));
|
|
@@ -5261,7 +5314,10 @@ export async function startPlugin(activate, options = {}) {
|
|
|
5261
5314
|
mkdirSync(data, { recursive: true });
|
|
5262
5315
|
if (host.fileRoots.length === 0)
|
|
5263
5316
|
host.fileRoots = [directory, data];
|
|
5264
|
-
const instance = await createPlugin(host, manifest, directory, data,
|
|
5317
|
+
const instance = await createPlugin(host, manifest, directory, data,
|
|
5318
|
+
// Like the player: protocol additions it takes (`app.features` set on
|
|
5319
|
+
// the host replaces them, e.g. `[]` for an older player's behaviour).
|
|
5320
|
+
{ features: [PATCH_FEATURE], ...host.app });
|
|
5265
5321
|
await activate(instance.plugin);
|
|
5266
5322
|
// Like the player: a position or resolver another plugin holds is not an
|
|
5267
5323
|
// activation failure.
|
|
@@ -5290,3 +5346,72 @@ export async function startPlugin(activate, options = {}) {
|
|
|
5290
5346
|
},
|
|
5291
5347
|
};
|
|
5292
5348
|
}
|
|
5349
|
+
/**
|
|
5350
|
+
* A patch's ops checked against an array of `length` rows, as the player
|
|
5351
|
+
* checks them (`ui_patch.rs`): in order, each on the result of the ones
|
|
5352
|
+
* before; at most {@link PATCH_OPS}; the result at most
|
|
5353
|
+
* `UI_VALUE_LIMITS.arrayRows` rows.
|
|
5354
|
+
*/
|
|
5355
|
+
function patchOps(ops, length) {
|
|
5356
|
+
if (!Array.isArray(ops))
|
|
5357
|
+
throw new HostError("invalid", "patch.ops requires an array");
|
|
5358
|
+
if (ops.length > PATCH_OPS)
|
|
5359
|
+
throw new HostError("limit", `a patch carries at most ${PATCH_OPS} operations`);
|
|
5360
|
+
const index = (op, key) => {
|
|
5361
|
+
const value = op[key];
|
|
5362
|
+
if (!Number.isInteger(value) || value < 0)
|
|
5363
|
+
throw new HostError("invalid", `patch op ${key} requires a non-negative integer`);
|
|
5364
|
+
return value;
|
|
5365
|
+
};
|
|
5366
|
+
const count = (op) => {
|
|
5367
|
+
const value = index(op, "count");
|
|
5368
|
+
if (value === 0)
|
|
5369
|
+
throw new HostError("invalid", "patch op count must be at least 1");
|
|
5370
|
+
return value;
|
|
5371
|
+
};
|
|
5372
|
+
const past = (what) => new HostError("invalid", `${what} is past the end of the list`);
|
|
5373
|
+
let len = length;
|
|
5374
|
+
return ops.map((raw) => {
|
|
5375
|
+
const op = (raw ?? {});
|
|
5376
|
+
switch (op.op) {
|
|
5377
|
+
case "set": {
|
|
5378
|
+
const at = index(op, "at");
|
|
5379
|
+
if (at >= len)
|
|
5380
|
+
throw past(`set at ${at}`);
|
|
5381
|
+
if (!("row" in op))
|
|
5382
|
+
throw new HostError("invalid", "patch op set requires row");
|
|
5383
|
+
return { op: "set", at, row: op.row };
|
|
5384
|
+
}
|
|
5385
|
+
case "insert": {
|
|
5386
|
+
const at = index(op, "at");
|
|
5387
|
+
const rows = op.rows;
|
|
5388
|
+
if (!Array.isArray(rows) || rows.length === 0)
|
|
5389
|
+
throw new HostError("invalid", "patch op insert requires rows");
|
|
5390
|
+
if (at > len)
|
|
5391
|
+
throw past(`insert at ${at}`);
|
|
5392
|
+
len += rows.length;
|
|
5393
|
+
if (len > UI_VALUE_LIMITS.arrayRows)
|
|
5394
|
+
throw new HostError("limit", `UI array exceeds ${UI_VALUE_LIMITS.arrayRows} rows`);
|
|
5395
|
+
return { op: "insert", at, rows: rows };
|
|
5396
|
+
}
|
|
5397
|
+
case "remove": {
|
|
5398
|
+
const at = index(op, "at");
|
|
5399
|
+
const n = count(op);
|
|
5400
|
+
if (at + n > len)
|
|
5401
|
+
throw past(`remove ${n} at ${at}`);
|
|
5402
|
+
len -= n;
|
|
5403
|
+
return { op: "remove", at, count: n };
|
|
5404
|
+
}
|
|
5405
|
+
case "move": {
|
|
5406
|
+
const from = index(op, "from");
|
|
5407
|
+
const n = count(op);
|
|
5408
|
+
const to = index(op, "to");
|
|
5409
|
+
if (from + n > len || to + n > len)
|
|
5410
|
+
throw past(`move ${n} from ${from} to ${to}`);
|
|
5411
|
+
return { op: "move", from, count: n, to };
|
|
5412
|
+
}
|
|
5413
|
+
default:
|
|
5414
|
+
throw new HostError("invalid", "patch op must be set, insert, remove or move");
|
|
5415
|
+
}
|
|
5416
|
+
});
|
|
5417
|
+
}
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -565,14 +565,16 @@ export interface CommandState {
|
|
|
565
565
|
reason: TextValue | null;
|
|
566
566
|
}
|
|
567
567
|
/**
|
|
568
|
-
* A value, or a function the SDK re-evaluates on every update pass
|
|
569
|
-
*
|
|
568
|
+
* A value, or a function the SDK re-evaluates on every full update pass,
|
|
569
|
+
* and after a player state change only if it read a field of
|
|
570
|
+
* `plugin.player.state` that changed (see {@link Scope.update}); only
|
|
571
|
+
* changes are sent to the player.
|
|
570
572
|
*
|
|
571
573
|
* @example Mix a computed state with a fixed one
|
|
572
574
|
* The Loop item carries a check mark while a loop is set and is always
|
|
573
|
-
* enabled. A function is evaluated again in every update pass (after
|
|
574
|
-
* handler and player state change
|
|
575
|
-
* value never changes.
|
|
575
|
+
* enabled. A function is evaluated again in every full update pass (after
|
|
576
|
+
* each handler), and after a player state change only if it read a changed
|
|
577
|
+
* field; only changes are sent, and a plain value never changes.
|
|
576
578
|
* ```ts
|
|
577
579
|
* plugin.commands.add({
|
|
578
580
|
* id: "loop",
|
|
@@ -1060,8 +1062,9 @@ export interface CommandOptions {
|
|
|
1060
1062
|
* @remarks
|
|
1061
1063
|
* The handle of one menu item and its keys. Its state (`enabled`, `checked`,
|
|
1062
1064
|
* `busy`, `reason`) is bound: functions given to {@link Commands.add} or
|
|
1063
|
-
* {@link Command.set} are evaluated again in every update pass
|
|
1064
|
-
*
|
|
1065
|
+
* {@link Command.set} are evaluated again in every full update pass (after
|
|
1066
|
+
* a player state change only if they read a changed field of
|
|
1067
|
+
* `plugin.player.state`) and only changes reach the player. Its title, path, bindings and button cannot
|
|
1065
1068
|
* change in place; dispose the command (or a child {@link Scope}) and add it
|
|
1066
1069
|
* again. Disposing it, its scope or stopping the plugin removes the menu
|
|
1067
1070
|
* item, its keys and its bar button.
|
|
@@ -1081,7 +1084,7 @@ export interface CommandOptions {
|
|
|
1081
1084
|
* path: ["@video", "Export…"],
|
|
1082
1085
|
* run: runExport,
|
|
1083
1086
|
* });
|
|
1084
|
-
* // A function: re-evaluated in every
|
|
1087
|
+
* // A function: re-evaluated in every full pass and when `phase` changes.
|
|
1085
1088
|
* exportCommand.set({ enabled: () => plugin.player.state.phase === "ready" });
|
|
1086
1089
|
* ```
|
|
1087
1090
|
*
|
|
@@ -1706,8 +1709,9 @@ export interface Ui {
|
|
|
1706
1709
|
* callback.
|
|
1707
1710
|
*
|
|
1708
1711
|
* `bind` is the usual way: the SDK re-evaluates bound functions in every
|
|
1709
|
-
* update pass (after each handler it calls,
|
|
1710
|
-
*
|
|
1712
|
+
* full update pass (after each handler it calls, on {@link Scope.update}),
|
|
1713
|
+
* and after a player state change those that read a changed field of
|
|
1714
|
+
* `plugin.player.state`, and sends only what changed, at most one write in
|
|
1711
1715
|
* flight per view. `set` sends now, even an unchanged value.
|
|
1712
1716
|
*
|
|
1713
1717
|
* Writes that fail (a name the Slint file does not declare, a value of the
|
|
@@ -1843,7 +1847,11 @@ export interface View {
|
|
|
1843
1847
|
*
|
|
1844
1848
|
* @remarks
|
|
1845
1849
|
* The SDK calls `props` in every update pass (and right after `bind`) and
|
|
1846
|
-
* sends the values that differ from the ones it sent last.
|
|
1850
|
+
* sends the values that differ from the ones it sent last. After a change
|
|
1851
|
+
* of the player state alone it calls `props` only if it read a field of
|
|
1852
|
+
* `plugin.player.state` that changed (SDK 1.5.45); a full pass follows
|
|
1853
|
+
* after each handler, on {@link Scope.update} and at least once a second
|
|
1854
|
+
* while the player state keeps changing. Because an
|
|
1847
1855
|
* unchanged value is not sent again, text the user typed into an `in-out`
|
|
1848
1856
|
* property stays until the plugin's value for it changes.
|
|
1849
1857
|
*
|
|
@@ -1864,8 +1872,9 @@ export interface View {
|
|
|
1864
1872
|
* @example Show what is playing and whether it can seek
|
|
1865
1873
|
* The status window always shows the current file and enables its seek
|
|
1866
1874
|
* controls only for seekable sources. The SDK calls the function right
|
|
1867
|
-
* away
|
|
1868
|
-
*
|
|
1875
|
+
* away, again in every full update pass (after each handler) and after
|
|
1876
|
+
* each change of `source` or `can`, and sends only the values that
|
|
1877
|
+
* changed.
|
|
1869
1878
|
* ```ts
|
|
1870
1879
|
* const status = plugin.windows.create("ui/status.slint");
|
|
1871
1880
|
* status.bind(() => ({
|
|
@@ -5697,8 +5706,8 @@ export interface Player {
|
|
|
5697
5706
|
* @example Drive the player's Next and Previous from a playlist
|
|
5698
5707
|
* The player's Next and Previous items and keys play the plugin's list.
|
|
5699
5708
|
* The player calls `next` and `previous` when the user asks; `canNext`
|
|
5700
|
-
* and `canPrevious` are evaluated again after every handler the SDK runs
|
|
5701
|
-
* and while they return false the player disables its items ("No next
|
|
5709
|
+
* and `canPrevious` are evaluated again after every handler the SDK runs
|
|
5710
|
+
* (and after a player state change if they read a changed field), and while they return false the player disables its items ("No next
|
|
5702
5711
|
* item").
|
|
5703
5712
|
* ```ts
|
|
5704
5713
|
* declare const items: string[];
|
package/dist/plugin/ui.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Json } from "../wire.ts";
|
|
1
|
+
import type { Json, PropertyWrite } from "../wire.ts";
|
|
2
2
|
import type { ScopeImpl } from "./scope.ts";
|
|
3
3
|
import type { Dialogs, Disposable, ImageOptions, ImageRef, Panels, PropValue, Props, Ui, View, Windows } from "./types.ts";
|
|
4
4
|
/** Slint names are kebab-case; plugin code may use camelCase. */
|
|
@@ -29,7 +29,7 @@ export declare abstract class ViewBase implements View {
|
|
|
29
29
|
protected closed: boolean;
|
|
30
30
|
constructor(scope: ScopeImpl);
|
|
31
31
|
protected abstract live(): boolean;
|
|
32
|
-
protected abstract write(property: string,
|
|
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
35
|
* property keeps its default) and reported, like a failed write.
|
|
@@ -38,7 +38,11 @@ export declare abstract class ViewBase implements View {
|
|
|
38
38
|
on<A extends Json[]>(callback: string, handler: (...args: A) => unknown): Disposable;
|
|
39
39
|
set(props: Props): void;
|
|
40
40
|
bind(props: () => Props): Disposable;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Current desired properties (bound values win over earlier `set`). A
|
|
43
|
+
* precise pass (`Runtime.part`) skips a bind function that read none of
|
|
44
|
+
* the changed state: its last values stay.
|
|
45
|
+
*/
|
|
42
46
|
protected snapshot(): Record<string, PropValue>;
|
|
43
47
|
protected evaluate(): void;
|
|
44
48
|
/** The host already shows these values (e.g. sent as initial properties). */
|