@vestra-player/sdk 1.5.29 → 1.5.30

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.
@@ -33,7 +33,11 @@ export type { Json, AudioNode, VideoNode, SubtitleStyle, ResolvedMedia };
33
33
  * unsubscribed. A {@link Media} is the exception: disposing its scope does
34
34
  * **not** close the media; call {@link Media.close} for that.
35
35
  *
36
- * @example Releasing a listener early
36
+ * @example Stop listening once the file is ready
37
+ * The listener reacts to the first time the source becomes ready and then
38
+ * stops being called. Every registration returns a `Disposable`;
39
+ * disposing it removes just that listener, before the scope that owns it
40
+ * ends.
37
41
  * ```ts
38
42
  * const subscription = plugin.player.onChange((state) => {
39
43
  * if (state.phase === "ready") void subscription.dispose();
@@ -100,7 +104,11 @@ export interface Disposable {
100
104
  * the plugin process. Requests have no client-side timeout: the player
101
105
  * answers every request or stops the plugin.
102
106
  *
103
- * @example
107
+ * @example Ignore a seek that lost a race, rethrow anything else
108
+ * The plugin seeks to 0:30 and stays quiet when another file was opened in
109
+ * between. Every failure is an `Error` with a stable `code`; `stale` means
110
+ * the source changed since the state the call was based on and nothing
111
+ * was done, so it is safe to drop. Other codes are real failures.
104
112
  * ```ts
105
113
  * try {
106
114
  * await plugin.player.seek(30);
@@ -129,7 +137,11 @@ export type PluginError = Error & {
129
137
  * `.theme`, {@link Scope.ui}, {@link Scope.tools}, {@link Scope.storage} and
130
138
  * {@link Scope.system} needs none.
131
139
  *
132
- * @example Rebuilding a dynamic submenu with a child scope
140
+ * @example Rebuild a dynamic submenu with a child scope
141
+ * Subtitles ▸ Track lists the current file's subtitle tracks and is
142
+ * rebuilt whenever they change. Commands added through a child scope
143
+ * belong to it; disposing the scope removes all of them at once, and a
144
+ * new child scope collects the next set.
133
145
  * ```ts
134
146
  * let tracks = plugin.scope();
135
147
  * plugin.subtitles.onChange(async (state) => {
@@ -263,9 +275,12 @@ export interface Scope extends Disposable {
263
275
  * @returns A handle that cancels the registration early (without
264
276
  * running `cleanup`).
265
277
  *
266
- * @example
278
+ * @example Stop a timer when the plugin stops
279
+ * A once-a-second refresh runs as long as the plugin does. `onDispose`
280
+ * runs the cleanup when the scope is disposed, which for `plugin` is
281
+ * when the player stops the plugin, so the timer never outlives it.
267
282
  * ```ts
268
- * const timer = setInterval(() => plugin.update(), 1000);
283
+ * const timer = setInterval(() => plugin.update(), 1000); // re-evaluate bindings
269
284
  * plugin.onDispose(() => clearInterval(timer));
270
285
  * ```
271
286
  */
@@ -279,10 +294,14 @@ export interface Scope extends Disposable {
279
294
  * registrations made after activation (a command whose `ready` rejects,
280
295
  * a property write the player refused).
281
296
  *
282
- * @example
297
+ * @example Show errors from handlers on the OSD
298
+ * When a menu command, a window callback or a listener of the plugin
299
+ * fails, the user sees why. The SDK catches what those handlers throw
300
+ * (and their rejected promises) and passes it here instead of only
301
+ * logging it; the message is data in a translated template.
283
302
  * ```ts
284
303
  * plugin.onError((error) => {
285
- * void plugin.osd.show(`Bookmarks: ${error.message}`);
304
+ * void plugin.osd.show("Bookmarks: {}", [error.message]);
286
305
  * });
287
306
  * ```
288
307
  */
@@ -315,7 +334,11 @@ export interface Scope extends Disposable {
315
334
  * process that has not exited after 300 ms is killed. Media the plugin
316
335
  * opened with {@link Player.open} keeps playing.
317
336
  *
318
- * @example A minimal plugin: main.ts, with capabilities commands and ui.osd
337
+ * @example Write a minimal plugin with one menu item
338
+ * Tools ▸ Say hello, or Ctrl+H, shows "Hello from" and the plugin's id.
339
+ * The player imports `main.ts`, calls its default export with the
340
+ * `Plugin` object, and keeps the registrations made during activation;
341
+ * the manifest declares the capabilities `commands` and `ui.osd`.
319
342
  * ```ts
320
343
  * import type { Plugin } from "@vestra-player/sdk";
321
344
  *
@@ -323,8 +346,8 @@ export interface Scope extends Disposable {
323
346
  * plugin.commands.add({
324
347
  * id: "hello",
325
348
  * path: ["Tools", "Say hello"],
326
- * bindings: ["Ctrl+H"],
327
- * run: () => plugin.osd.show(`Hello from ${plugin.id}`),
349
+ * bindings: ["Ctrl+H"], // a default the user can change
350
+ * run: () => plugin.osd.show("Hello from {}", [plugin.id]),
328
351
  * });
329
352
  * }
330
353
  * ```
@@ -367,7 +390,11 @@ export interface CommandState {
367
390
  * A value, or a function the SDK re-evaluates on every update pass (see
368
391
  * {@link Scope.update}); only changes are sent to the player.
369
392
  *
370
- * @example
393
+ * @example Mix a computed state with a fixed one
394
+ * The Loop item carries a check mark while a loop is set and is always
395
+ * enabled. A function is evaluated again in every update pass (after each
396
+ * handler and player state change) and only changes are sent; a plain
397
+ * value never changes.
371
398
  * ```ts
372
399
  * plugin.commands.add({
373
400
  * id: "loop",
@@ -424,12 +451,17 @@ export type BarButtonIcon = "play" | "pause" | "stop" | "back" | "forward" | "op
424
451
  * with its command: disposing the command, or disabling, stopping or
425
452
  * uninstalling the plugin, removes it.
426
453
  *
427
- * @example
454
+ * @example Give a command a button in the bottom bar
455
+ * A Store icon appears in the bottom bar's button strip; a click opens
456
+ * the store window, exactly like the menu item. The player draws the
457
+ * button from the command's title, keys and state, and places it by
458
+ * `order` (200 is where the player's own Plugin Store button sits); a
459
+ * bar that does not place `BarButtons` shows only the menu item.
428
460
  * ```ts
429
461
  * const store = plugin.windows.create("ui/store.slint");
430
462
  * plugin.commands.add({
431
463
  * id: "open",
432
- * path: ["Plugin Store…"],
464
+ * path: ["Plugin Store…"], // a button needs a menu path
433
465
  * button: { icon: "store", order: 200 },
434
466
  * run: () => store.show(),
435
467
  * });
@@ -515,10 +547,19 @@ export interface CommandOptions {
515
547
  * ({})", [n, codec])`. The menu shows it translated and filled in; it
516
548
  * merges by its English rendering ("Track 2 (opus)").
517
549
  *
518
- * @example
550
+ * @example Put items into built-in menus, one with a track number
551
+ * "Blur" appears in the Video menu and "Track 2 (opus)" under Audio ▸
552
+ * Audio Track, both in the user's language. `@video` and `@audio` name
553
+ * the built-in menus by id; "Audio Track" is a submenu key that merges
554
+ * with any other plugin's same key; the `text()` segment is translated
555
+ * with the plugin's catalog and then filled in with the number and codec.
519
556
  * ```ts
557
+ * declare function toggle(): void;
558
+ * declare function select(): void;
559
+ * // ---cut---
520
560
  * plugin.commands.add({ id: "blur", path: ["@video", "Blur"], run: toggle });
521
561
  * const { text } = plugin.i18n;
562
+ * // A segment with data: translated as "Track {} ({})", then filled in.
522
563
  * plugin.commands.add({
523
564
  * id: "track-2",
524
565
  * path: ["@audio", "Audio Track", text("Track {} ({})", [2, "opus"])],
@@ -610,9 +651,26 @@ export interface Command extends Disposable {
610
651
  /**
611
652
  * Replace some state fields (values or functions). Last write wins.
612
653
  *
613
- * @example
654
+ * @example Show a command as busy while it works
655
+ * The Export item shows as busy with "Exporting…" as its reason while the
656
+ * export runs, then returns to normal. `set` replaces only the fields it
657
+ * names and the SDK sends the new state to the player; a busy command's
658
+ * menu item and key do not run it again until it is set back.
614
659
  * ```ts
615
- * exportCommand.set({ busy: true, reason: "Exporting…" });
660
+ * declare function runExport(): Promise<void>;
661
+ * // ---cut---
662
+ * const exportCommand = plugin.commands.add({
663
+ * id: "export",
664
+ * path: ["@video", "Export…"],
665
+ * run: async () => {
666
+ * exportCommand.set({ busy: true, reason: "Exporting…" });
667
+ * try {
668
+ * await runExport();
669
+ * } finally {
670
+ * exportCommand.set({ busy: false, reason: null }); // also on failure
671
+ * }
672
+ * },
673
+ * });
616
674
  * ```
617
675
  */
618
676
  set(state: Partial<{
@@ -640,12 +698,17 @@ export interface Commands {
640
698
  * unknown `@` menu id), order, group or binding; `conflict` for an id this
641
699
  * plugin has already declared and not disposed.
642
700
  *
643
- * @example
701
+ * @example Pause playback from a menu item
702
+ * Playback ▸ Pause (and Ctrl+P) pauses and resumes, shows a check mark
703
+ * while paused and is greyed out when nothing can be paused. The player
704
+ * calls `run` when the user picks the item or presses its key; `enabled`
705
+ * and `checked` are functions the SDK evaluates again after every
706
+ * handler and player state change, sending only what changed.
644
707
  * ```ts
645
708
  * const toggle = plugin.commands.add({
646
709
  * id: "toggle-pause",
647
710
  * path: ["Playback", "Pause"],
648
- * order: 210,
711
+ * order: 210, // position among the Playback menu's items
649
712
  * bindings: ["Ctrl+P"],
650
713
  * enabled: () => plugin.player.state.can.pause,
651
714
  * checked: () => plugin.player.state.paused,
@@ -663,8 +726,8 @@ export interface Commands {
663
726
  * {@link View.bind}: the whole value of an `image` property or, from SDK
664
727
  * 1.5.19 (player 0.5.1), an `image` field at any depth, such as the logo of
665
728
  * each row of a list. Making the reference does not read the file: the
666
- * player checks and decodes it each time a view uses it ({@link Ui.image}
667
- * has the rules and limits).
729
+ * player checks and decodes it each time it receives the value (a property
730
+ * write, a window opening; {@link Ui.image} has the rules and limits).
668
731
  *
669
732
  * A reference belongs to the {@link Scope} that created it. Once that scope
670
733
  * is disposed, writing the reference fails with `invalid` (reported to
@@ -672,13 +735,24 @@ export interface Commands {
672
735
  * property changes or the view closes.
673
736
  *
674
737
  * @example Show a cover image from the data directory
738
+ * A window opens showing `cover.png`, which the plugin saved in its data
739
+ * directory. `ui.image` only makes a reference, without reading the file;
740
+ * the player checks and decodes the file when it receives the value, so a
741
+ * missing file fails that write with `missing` (here: `show` rejects, as
742
+ * it is an initial property). A missing image nested inside a value is
743
+ * drawn empty instead.
675
744
  * ```ts
676
745
  * const cover = plugin.windows.create("ui/cover.slint");
746
+ * // `art` is an `image` property in ui/cover.slint.
677
747
  * cover.set({ art: plugin.ui.image(`${plugin.dataDirectory}/cover.png`) });
678
748
  * await cover.show();
679
749
  * ```
680
750
  *
681
- * @example A logo in each row
751
+ * @example Show a logo in each row of a list
752
+ * Each row of the list draws its own small logo next to its name. An
753
+ * `ImageRef` may sit in any field of a property value (SDK 1.5.19, player
754
+ * 0.5.1), so the rows carry their icons; `maxSide: 64` decodes each logo
755
+ * at most 64 px wide, which keeps the view's image memory small.
682
756
  * ```slint
683
757
  * export struct Row { key: string, name: string, icon: image }
684
758
  * ```
@@ -689,6 +763,7 @@ export interface Commands {
689
763
  * rows: items.map((item) => ({
690
764
  * key: item.id,
691
765
  * name: item.name,
766
+ * // About twice the drawn size (32 px) stays sharp on dense screens.
692
767
  * icon: plugin.ui.image(item.iconPath, { maxSide: 64 }),
693
768
  * })),
694
769
  * }));
@@ -739,7 +814,11 @@ export interface ImageOptions {
739
814
  * scope that created it: once that scope is disposed, writing it fails with
740
815
  * `invalid`, wherever it is in the value.
741
816
  *
742
- * @example Chat messages, one document per row
817
+ * @example Show chat messages, one document per row
818
+ * The chat window shows a formatted assistant message. `ui.markdown`
819
+ * resolves once the document is ready (here without downloading images);
820
+ * the reference goes into the `body` field of a row, whose Slint type is
821
+ * `[MarkdownBlock]`, and the player parses and draws it.
743
822
  * ```ts
744
823
  * const chat = plugin.windows.create("ui/chat.slint");
745
824
  * const body = await plugin.ui.markdown("**Hello**", { remoteImages: false });
@@ -833,7 +912,11 @@ export type PropValue = PropData | ImageRef;
833
912
  * JSON whose values may also be Markdown documents and (SDK 1.5.19) images,
834
913
  * at any depth: the part of {@link PropValue} that nests.
835
914
  *
836
- * @example A message list whose rows carry a document
915
+ * @example Type a message list whose rows carry a document
916
+ * A property value for a chat's message list, typed so that TypeScript
917
+ * accepts a Markdown document inside each row. `PropData` is the JSON a
918
+ * property takes plus documents and images at any depth; the player turns
919
+ * each object into a Slint struct by its field names.
837
920
  * ```ts
838
921
  * const messages: PropData = [
839
922
  * { id: "m1", role: "user", body: await plugin.ui.markdown("Hi") },
@@ -855,7 +938,11 @@ export type PropData = null | boolean | number | string | MarkdownRef | ImageRef
855
938
  * kebab-case are kept. Only the property names are converted, not the
856
939
  * fields of struct values.
857
940
  *
858
- * @example
941
+ * @example Prepare a window's initial properties
942
+ * An object of property values by name, as `windows.create` and
943
+ * `View.set` take them. Names match the Slint file (`canMark` for
944
+ * `can-mark`), and each value becomes the Slint type of its property: a
945
+ * boolean, and an array of structs for a model.
859
946
  * ```ts
860
947
  * const props: Props = { canMark: true, rows: [{ rowId: "a", title: "First" }] };
861
948
  * ```
@@ -879,7 +966,13 @@ export type Props = Record<string, PropValue>;
879
966
  *
880
967
  * @capability None.
881
968
  *
882
- * @example A window that shows a Markdown file (examples/markdown)
969
+ * @example Show a Markdown file in a window (examples/markdown)
970
+ * A menu item opens a window with a formatted Markdown file that ships
971
+ * with the plugin. The plugin reads the file, turns it into a document
972
+ * with `ui.markdown` (relative images resolve against the plugin's
973
+ * directory through `base`) and writes it to the window's `doc` property,
974
+ * which the Slint file hands to `MarkdownView`.
975
+ *
883
976
  * {@includeCode ../../examples/markdown/main.ts#document}
884
977
  *
885
978
  * `ui/document.slint`:
@@ -896,8 +989,8 @@ export interface Ui {
896
989
  *
897
990
  * @remarks
898
991
  * Returns at once without touching the file. The player checks the file
899
- * each time a view uses the reference (a property write, a window opening,
900
- * the main window being rebuilt): it must be inside this plugin's package
992
+ * each time it receives the reference (a property write, a window or
993
+ * settings page opening, the main window being rebuilt): it must be inside this plugin's package
901
994
  * directory or {@link Plugin.dataDirectory} after following links, at most
902
995
  * 16 MB and 4096×4096 pixels, and PNG or JPEG by content (the extension
903
996
  * does not matter). A failed check of a whole-value image is a failed
@@ -909,9 +1002,10 @@ export interface Ui {
909
1002
  * the value is shown, as a Markdown image shows its alt text.
910
1003
  *
911
1004
  * The player caches decoded images per plugin (up to 64 MB, least recently
912
- * used first) and does not watch files. Every call returns a new value, so
913
- * after rewriting a file under the same path, call `image()` again and
914
- * write the new reference.
1005
+ * used first) by path, size and modification time, and does not watch
1006
+ * files. After rewriting a file under the same path, call `image()` again
1007
+ * and write the new reference: the SDK does not send a value that did not
1008
+ * change, so the player would not look at the file again.
915
1009
  *
916
1010
  * @capability None.
917
1011
  *
@@ -925,6 +1019,12 @@ export interface Ui {
925
1019
  * `maxSide` is not an integer 1–4096.
926
1020
  *
927
1021
  * @example Show a thumbnail the plugin rendered
1022
+ * A window shows a PNG the plugin wrote into its data directory. The file
1023
+ * must be inside the package or data directory; `image` returns at once.
1024
+ * The player decodes the file when it receives the value and notices a
1025
+ * rewritten file by its size and modification time; the plugin still
1026
+ * calls `image()` again after rewriting it, because the SDK does not send
1027
+ * a value that did not change.
928
1028
  * ```ts
929
1029
  * const window = plugin.windows.create("ui/thumbnail.slint");
930
1030
  * const file = `${plugin.dataDirectory}/thumbnail.png`;
@@ -962,7 +1062,12 @@ export interface Ui {
962
1062
  * large, or is not a PNG or JPEG image; or the plugin has no data
963
1063
  * directory.
964
1064
  *
965
- * @example Search results with thumbnails
1065
+ * @example Show search results with downloaded thumbnails
1066
+ * A results window lists each hit with its thumbnail, or a placeholder
1067
+ * when a thumbnail cannot be fetched. `remoteImage` downloads the image
1068
+ * in the plugin's process into the data directory's cache (at most 4 at a
1069
+ * time) and resolves with a local reference; a failed download rejects
1070
+ * with `unavailable`, which the `catch` turns into the placeholder.
966
1071
  * ```ts
967
1072
  * const results = [{ title: "A", thumbnailUrl: "https://example.com/a.jpg" }];
968
1073
  * const placeholder = plugin.ui.image(`${plugin.dataDirectory}/blank.png`);
@@ -971,7 +1076,7 @@ export interface Ui {
971
1076
  * title: r.title,
972
1077
  * thumb: await plugin.ui
973
1078
  * .remoteImage(r.thumbnailUrl, { maxSide: 320 })
974
- * .catch(() => placeholder),
1079
+ * .catch(() => placeholder), // `unavailable`: download failed
975
1080
  * })),
976
1081
  * );
977
1082
  * plugin.windows.create("ui/results.slint").set({ rows });
@@ -1024,7 +1129,14 @@ export interface Ui {
1024
1129
  * `base` is neither an `http(s):` address nor an absolute path.
1025
1130
  * @throws `unsupported` (rejects) The player is older than 0.2.0.
1026
1131
  *
1027
- * @example A chat: one Markdown document per row, with Copy buttons
1132
+ * @example Build a chat with one Markdown document per row
1133
+ * A chat window lists messages as formatted text, with a Copy button on
1134
+ * each code block that puts the code on the clipboard. Each row's `body`
1135
+ * is a document from `ui.markdown`; the whole list is written again on
1136
+ * each change, and the player parses only the documents that changed.
1137
+ * `MarkdownBlocks` with `code-copy` calls `copy-code`, which the plugin
1138
+ * handles with `on("copyCode")`.
1139
+ *
1028
1140
  * `ui/chat.slint`:
1029
1141
  *
1030
1142
  * ```slint
@@ -1051,6 +1163,7 @@ export interface Ui {
1051
1163
  * const chat = plugin.windows.create("ui/chat.slint");
1052
1164
  * const history: { id: string; role: string; text: string }[] = [];
1053
1165
  * chat.on("copyCode", (code: string) => plugin.clipboard.writeText(code));
1166
+ * // Re-send the whole list: unchanged documents cost the player nothing.
1054
1167
  * async function render() {
1055
1168
  * const messages = [];
1056
1169
  * for (const { id, role, text } of history)
@@ -1063,10 +1176,19 @@ export interface Ui {
1063
1176
  * ```
1064
1177
  *
1065
1178
  * @example Show a README from the web
1179
+ * A window shows a README downloaded from the web, its images included,
1180
+ * and clicked links open in the browser. `base` makes relative links and
1181
+ * images resolve against the README's address; `markdown` resolves after
1182
+ * its `https:` images are downloaded. A click on a link makes
1183
+ * `MarkdownView` call `link-clicked`, which `ui/readme.slint` forwards as
1184
+ * `open-link`; the plugin opens the address with `openUrl`.
1066
1185
  * ```ts
1067
1186
  * const readmeUrl = "https://example.com/plugin/README.md";
1068
- * const text = await (await fetch(readmeUrl)).text();
1187
+ * const response = await fetch(readmeUrl);
1188
+ * if (!response.ok) throw new Error(`README: HTTP ${response.status}`);
1189
+ * const text = await response.text();
1069
1190
  * const window = plugin.windows.create("ui/readme.slint");
1191
+ * // Bound before the first `show`, as a tool window requires.
1070
1192
  * window.on("openLink", (url: string) => plugin.system.openUrl(url));
1071
1193
  * window.set({ doc: await plugin.ui.markdown(text, { base: readmeUrl }) });
1072
1194
  * await window.show();
@@ -1109,7 +1231,13 @@ export interface Ui {
1109
1231
  *
1110
1232
  * @capability That of the view: `ui.window`, `ui.mount` or `ui.settings`.
1111
1233
  *
1112
- * @example A window with a label, two buttons and their callbacks (examples/bookmark)
1234
+ * @example Wire a window's label and buttons to the plugin (examples/bookmark)
1235
+ * A Bookmark window shows the saved position and two buttons that save
1236
+ * it and jump back to it. `bind` keeps the label and the buttons' enabled
1237
+ * state equal to the plugin's state: the SDK evaluates the function again
1238
+ * after every handler and player change and sends only what changed. The
1239
+ * buttons' Slint callbacks reach the plugin through `on`.
1240
+ *
1113
1241
  * {@includeCode ../../examples/bookmark/main.ts#window}
1114
1242
  *
1115
1243
  * `ui/bookmark.slint`:
@@ -1154,10 +1282,16 @@ export interface View {
1154
1282
  * @throws `Error` without a code (throws) On an open {@link Window}, for a
1155
1283
  * name that has no handler yet.
1156
1284
  *
1157
- * @example
1285
+ * @example Keep a slider and the plugin's value in step
1286
+ * Moving the window's brightness slider updates the plugin's value, and
1287
+ * the slider shows it back. The Slint callback `brightness-changed`
1288
+ * reaches the handler with the slider's value as JSON; after the handler
1289
+ * returns, the SDK runs an update pass and `bind` sends the new value.
1290
+ * Callbacks of a tool window must be bound before its first `show`.
1158
1291
  * ```ts
1159
1292
  * const picture = plugin.windows.create("ui/picture.slint");
1160
1293
  * let brightness = 0;
1294
+ * // camelCase here, `brightness-changed` in the Slint file.
1161
1295
  * picture.on("brightnessChanged", (value: number) => {
1162
1296
  * brightness = value;
1163
1297
  * });
@@ -1196,6 +1330,10 @@ export interface View {
1196
1330
  * @param props - Properties by name; see {@link Props}.
1197
1331
  *
1198
1332
  * @example Put a drop-down back when the user picks an entry the plugin refuses
1333
+ * Choosing "Custom" snaps the drop-down back to the last real preset.
1334
+ * The plugin cannot read a property back, only hear the callback; `set`
1335
+ * sends the value even though it equals the one sent before, which is
1336
+ * what pulls a control the user moved back to the plugin's value.
1199
1337
  * ```ts
1200
1338
  * const window = plugin.windows.create("ui/presets.slint");
1201
1339
  * let presetIndex = 0;
@@ -1231,7 +1369,11 @@ export interface View {
1231
1369
  * @param props - Returns the properties, by name.
1232
1370
  * @returns A handle that stops this binding (the values last sent stay).
1233
1371
  *
1234
- * @example
1372
+ * @example Show what is playing and whether it can seek
1373
+ * The status window always shows the current file and enables its seek
1374
+ * controls only for seekable sources. The SDK calls the function right
1375
+ * away and again in every update pass (after player state changes and
1376
+ * after each handler) and sends only the values that changed.
1235
1377
  * ```ts
1236
1378
  * const status = plugin.windows.create("ui/status.slint");
1237
1379
  * status.bind(() => ({
@@ -1281,10 +1423,19 @@ export interface View {
1281
1423
  *
1282
1424
  * @capability ui.window
1283
1425
  *
1284
- * @example A menu command that opens a window (examples/bookmark)
1426
+ * @example Open a window from a menu command (examples/bookmark)
1427
+ * Playback ▸ Bookmark… opens the Bookmark window. `create` only makes a
1428
+ * handle; the command's `run` calls `show`, which compiles the Slint file
1429
+ * and opens the window with the bound properties the first time, and
1430
+ * brings back the same window afterwards.
1431
+ *
1285
1432
  * {@includeCode ../../examples/bookmark/main.ts#window,command}
1286
1433
  *
1287
1434
  * @example Stop work when the user closes the window
1435
+ * A clock window ticks every second while it is open and stops costing
1436
+ * anything once the user closes it. `plugin.update()` makes the SDK
1437
+ * evaluate the bound time again from a timer; `onClose` runs only for the
1438
+ * user's close, so the plugin stops the timer there.
1288
1439
  * ```ts
1289
1440
  * const clock = plugin.windows.create("ui/clock.slint");
1290
1441
  * let timer: NodeJS.Timeout | undefined;
@@ -1298,6 +1449,7 @@ export interface View {
1298
1449
  * path: ["@screen", "Clock…"],
1299
1450
  * run: async () => {
1300
1451
  * await clock.show();
1452
+ * // Re-evaluate `bind` once a second: bound values change only then.
1301
1453
  * timer ??= setInterval(() => plugin.update(), 1000);
1302
1454
  * },
1303
1455
  * });
@@ -1339,12 +1491,17 @@ export interface Window extends View, Disposable {
1339
1491
  * JPEG.
1340
1492
  *
1341
1493
  * @example Tell the user why a window did not open
1494
+ * When the inspector cannot open, the user sees why on the OSD instead
1495
+ * of nothing happening. `show` compiles the Slint file and opens the
1496
+ * window; it rejects with a code, such as `host` for a Slint file that
1497
+ * does not compile or `unavailable` without a user interface.
1342
1498
  * ```ts
1343
1499
  * const window = plugin.windows.create("ui/inspector.slint");
1344
1500
  * try {
1345
1501
  * await window.show();
1346
1502
  * } catch (error) {
1347
- * await plugin.osd.show(`Cannot open the inspector (${(error as PluginError).code})`);
1503
+ * // The code is data; `message` has the details for the log.
1504
+ * await plugin.osd.show("Cannot open the inspector ({})", [(error as PluginError).code]);
1348
1505
  * }
1349
1506
  * ```
1350
1507
  *
@@ -1366,10 +1523,14 @@ export interface Window extends View, Disposable {
1366
1523
  * @throws `missing` (rejects) The user closed the window just before (the
1367
1524
  * close had not reached the plugin yet).
1368
1525
  *
1369
- * @example A command that shows and hides a window
1526
+ * @example Show and hide a window from a checkable menu item
1527
+ * Audio ▸ Levels shows the window with a check mark and hides it again.
1528
+ * `hide` keeps the window's instance and state, so showing it again is
1529
+ * instant; the check mark follows `visible`, and `onClose` resets it when
1530
+ * the user closes the window with its title-bar button.
1370
1531
  * ```ts
1371
1532
  * const levels = plugin.windows.create("ui/levels.slint");
1372
- * let visible = false;
1533
+ * let visible = false; // what the check mark shows
1373
1534
  * plugin.commands.add({
1374
1535
  * id: "levels",
1375
1536
  * path: ["@audio", "Levels"],
@@ -1402,6 +1563,10 @@ export interface Window extends View, Disposable {
1402
1563
  * @throws `missing` (rejects) The user closed the window just before.
1403
1564
  *
1404
1565
  * @example Close the window from its own button
1566
+ * The editor's Done button closes the editor window. `close` destroys
1567
+ * the window instance but keeps the handle, its handlers and bindings,
1568
+ * so the next `show` opens a fresh window; `onClose` is not called for
1569
+ * the plugin's own close.
1405
1570
  * ```ts
1406
1571
  * const editor = plugin.windows.create("ui/editor.slint");
1407
1572
  * editor.on("done", () => editor.close());
@@ -1434,7 +1599,12 @@ export interface Window extends View, Disposable {
1434
1599
  * @param listener - Called with no arguments; may be async.
1435
1600
  * @returns A handle that removes the listener.
1436
1601
  *
1437
- * @example Remember that the user closed it
1602
+ * @example Remember that the user closed the window
1603
+ * The plugin records that the notes window was closed, so it does not
1604
+ * reopen it at the next start. `onClose` runs for the title-bar button,
1605
+ * the player's close keys (Cmd+W, Ctrl+W, Ctrl+F4) and the system's own
1606
+ * close request such as Alt+F4 (not for the plugin's own `close`); the
1607
+ * store writes the change to `window.json` shortly after.
1438
1608
  * ```ts
1439
1609
  * const store = plugin.storage.json("window", { defaults: { open: false } });
1440
1610
  * const window = plugin.windows.create("ui/notes.slint");
@@ -1460,7 +1630,11 @@ export interface Window extends View, Disposable {
1460
1630
  *
1461
1631
  * @capability ui.window
1462
1632
  *
1463
- * @example
1633
+ * @example Create a window and open it from a menu (examples/bookmark)
1634
+ * A Bookmark window opens from Playback ▸ Bookmark…. `create` returns a
1635
+ * handle without opening anything; its bindings and callbacks are set up
1636
+ * first, and `show` opens the window when the command runs.
1637
+ *
1464
1638
  * {@includeCode ../../examples/bookmark/main.ts#window,command}
1465
1639
  *
1466
1640
  * @see {@link Window}, [Windows, panels, settings pages, dialogs and the OSD](../../docs/concepts/ui.md)
@@ -1495,7 +1669,10 @@ export interface Windows {
1495
1669
  * @throws `conflict` (throws) This plugin already has a window with this id
1496
1670
  * (SDK 1.3.0; before, a plain `Error` without `code`).
1497
1671
  *
1498
- * @example One file, two windows
1672
+ * @example Open two windows from one Slint file
1673
+ * Two compare windows open side by side, one labelled A and one B. A
1674
+ * window's id defaults to its file name, so the second window of the
1675
+ * same file needs its own `id`; each handle has its own properties.
1499
1676
  * ```ts
1500
1677
  * const left = plugin.windows.create("ui/compare.slint", { id: "compare-left" });
1501
1678
  * const right = plugin.windows.create("ui/compare.slint", { id: "compare-right" });
@@ -1596,12 +1773,18 @@ export interface PanelState {
1596
1773
  *
1597
1774
  * @capability ui.mount; `events` to follow changes made elsewhere.
1598
1775
  *
1599
- * @example A "Show sidebar" menu item that works with any sidebar panel
1776
+ * @example Add a "Show sidebar" item that works with any sidebar panel
1777
+ * Tools ▸ Show sidebar shows or hides the sidebar, whichever plugin's
1778
+ * panel is in it, with a check mark while it is shown. The position does
1779
+ * not belong to the plugin that mounted the panel: `state` is `null`
1780
+ * while nobody has a panel there (the item is disabled then), and
1781
+ * `toggle` flips the user's remembered preference.
1600
1782
  * ```ts
1601
1783
  * const sidebar = plugin.panels.sidebar;
1602
1784
  * plugin.commands.add({
1603
1785
  * id: "sidebar",
1604
1786
  * path: ["Tools", "Show sidebar"],
1787
+ * // `state` is null while no plugin has a panel in the sidebar.
1605
1788
  * checked: () => sidebar.state?.preferred ?? false,
1606
1789
  * enabled: () => sidebar.state !== null,
1607
1790
  * run: () => sidebar.toggle(),
@@ -1644,9 +1827,14 @@ export interface PanelSlot {
1644
1827
  * @param listener - Receives the new state.
1645
1828
  * @returns A handle that removes the listener.
1646
1829
  *
1647
- * @example
1830
+ * @example Tell the user when the window is too narrow for the sidebar
1831
+ * When the sidebar should show but the window has no room for it, the
1832
+ * OSD says so. With `events`, the listener runs on every change of the
1833
+ * position, whoever caused it. The player reports `reason: "space"` when
1834
+ * the preference is to show the sidebar but the window is too small.
1648
1835
  * ```ts
1649
1836
  * plugin.panels.sidebar.onChange((state) => {
1837
+ * // `null`: no panel is mounted in the sidebar any more.
1650
1838
  * if (state?.reason === "space") void plugin.osd.show("Widen the window to see the playlist");
1651
1839
  * });
1652
1840
  * ```
@@ -1672,9 +1860,15 @@ export interface PanelSlot {
1672
1860
  * @throws `unavailable` (rejects) The player runs without a user interface.
1673
1861
  * @throws `capability` (rejects) The manifest does not declare `ui.mount`.
1674
1862
  *
1675
- * @example
1863
+ * @example Hide the bottom bar for a clean picture
1864
+ * The control bar disappears, as if the user had hidden it from the menu,
1865
+ * and stays hidden at the next start because the preference is
1866
+ * remembered. `setVisible` rejects with `missing` when no panel is in the
1867
+ * bottom bar, which this plugin treats as nothing to hide.
1676
1868
  * ```ts
1677
- * await plugin.panels.bottom.setVisible(false);
1869
+ * await plugin.panels.bottom.setVisible(false).catch((error: PluginError) => {
1870
+ * if (error.code !== "missing") throw error; // no bar mounted: nothing to do
1871
+ * });
1678
1872
  * ```
1679
1873
  *
1680
1874
  * @since 1.0.0
@@ -1695,11 +1889,16 @@ export interface PanelSlot {
1695
1889
  * @throws `unavailable` (rejects) The player runs without a user interface.
1696
1890
  * @throws `capability` (rejects) The manifest does not declare `ui.mount`.
1697
1891
  *
1698
- * @example
1892
+ * @example Toggle the control bar from a checkable menu item
1893
+ * Screen ▸ Control bar shows or hides the bottom bar and carries a check
1894
+ * mark while it is shown. `toggle` reads the player's current preference
1895
+ * first, so it is right even without `events`; `checked` is evaluated
1896
+ * again after the command runs and follows `state`.
1699
1897
  * ```ts
1700
1898
  * plugin.commands.add({
1701
1899
  * id: "toggle-bar",
1702
1900
  * path: ["@screen", "Control bar"],
1901
+ * // The user's preference, remembered across launches.
1703
1902
  * checked: () => plugin.panels.bottom.state?.preferred ?? false,
1704
1903
  * run: () => plugin.panels.bottom.toggle(),
1705
1904
  * });
@@ -1731,7 +1930,11 @@ export interface PanelSlot {
1731
1930
  * @throws `unavailable` (rejects) The player runs without a user interface.
1732
1931
  * @throws `capability` (rejects) The manifest does not declare `ui.mount`.
1733
1932
  *
1734
- * @example A command that shows the playlist's tab
1933
+ * @example Switch the sidebar to the playlist's tab
1934
+ * A command makes the playlist the sidebar's current tab without showing
1935
+ * or hiding the sidebar, like a click on its tab. `activate` takes the
1936
+ * manifest id of the plugin whose tab to show and rejects with
1937
+ * `missing` when that plugin has no panel there.
1735
1938
  * ```ts
1736
1939
  * plugin.commands.add({
1737
1940
  * id: "to-playlist",
@@ -1757,12 +1960,17 @@ export interface PanelSlot {
1757
1960
  *
1758
1961
  * @capability ui.mount
1759
1962
  *
1760
- * @example A "Detach sidebar" menu item
1963
+ * @example Add a "Detach sidebar" item
1964
+ * Tools ▸ Detach sidebar floats the sidebar in its own window and docks
1965
+ * it again, with a check mark while it floats. `setDetached` moves all
1966
+ * the sidebar's tabs; the floating window holds new instances of the
1967
+ * panels, which keep the plugins' properties and callbacks.
1761
1968
  * ```ts
1762
1969
  * const sidebar = plugin.panels.sidebar;
1763
1970
  * plugin.commands.add({
1764
1971
  * id: "detach",
1765
1972
  * path: ["Tools", "Detach sidebar"],
1973
+ * // Detaching moves every tab of the sidebar into the floating window.
1766
1974
  * checked: () => sidebar.state?.detached ?? false,
1767
1975
  * enabled: () => sidebar.state !== null,
1768
1976
  * run: () => sidebar.setDetached(!sidebar.state?.detached),
@@ -1785,7 +1993,10 @@ export interface SidebarSlot extends PanelSlot {
1785
1993
  * @throws `unavailable` (rejects) The player runs without a user interface.
1786
1994
  * @throws `capability` (rejects) The manifest does not declare `ui.mount`.
1787
1995
  *
1788
- * @example
1996
+ * @example Float the sidebar or dock it back
1997
+ * The sidebar moves into its own window, or back into the main window
1998
+ * when it already floats. `state` says whether it is detached now;
1999
+ * `setDetached` resolves once the layout changed, with `state` refreshed.
1789
2000
  * ```ts
1790
2001
  * const sidebar = plugin.panels.sidebar;
1791
2002
  * await sidebar.setDetached(!sidebar.state?.detached);
@@ -1818,7 +2029,13 @@ export interface SidebarSlot extends PanelSlot {
1818
2029
  *
1819
2030
  * @capability ui.mount
1820
2031
  *
1821
- * @example A sidebar list with a right-click menu (examples/panel-menu)
2032
+ * @example Mount a sidebar list with a right-click menu (examples/panel-menu)
2033
+ * The sidebar gets an "Example list" tab whose rows can be selected and
2034
+ * have a context menu. `mount` returns at once and the player adds the
2035
+ * panel when it rebuilds its main window; `bind` keeps the rows and the
2036
+ * selection on the panel's bridge global, and the `context-menu` callback
2037
+ * opens a menu of the plugin's commands where the user clicked.
2038
+ *
1822
2039
  * {@includeCode ../../examples/panel-menu/main.ts#mount,menu}
1823
2040
  *
1824
2041
  * `ui/panel.slint`:
@@ -1893,7 +2110,12 @@ export interface Panel extends View, Disposable {
1893
2110
  * @param listener - Receives the new state.
1894
2111
  * @returns A handle that removes the listener.
1895
2112
  *
1896
- * @example Say when the user switched to another tab
2113
+ * @example Notice when the list moves to a background tab
2114
+ * The plugin logs when the user switches the sidebar to another plugin's
2115
+ * tab, for example to pause work the list does. With `events` the
2116
+ * listener runs on every change. `reason: "otherTab"` exists only in a
2117
+ * panel's own state: the SDK sets it when the sidebar's current tab is
2118
+ * another plugin's (the position's state never has it).
1897
2119
  * ```ts
1898
2120
  * const panel = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
1899
2121
  * panel.onChange((state) => {
@@ -1923,7 +2145,10 @@ export interface Panel extends View, Disposable {
1923
2145
  * @throws `unavailable` (rejects) The player runs without a user interface.
1924
2146
  * @throws The errors of {@link Panel.ready} (rejects) The mount failed.
1925
2147
  *
1926
- * @example
2148
+ * @example Bring the plugin's sidebar panel to the front
2149
+ * A command shows the List tab: it becomes the current tab and the
2150
+ * sidebar is shown, and the preference is remembered. `show` waits for
2151
+ * the mount first, so it can run right after `mount`.
1927
2152
  * ```ts
1928
2153
  * const list = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
1929
2154
  * plugin.commands.add({ id: "show", title: "Show the list", run: () => list.show() });
@@ -1951,13 +2176,18 @@ export interface Panel extends View, Disposable {
1951
2176
  * @throws `unavailable` (rejects) The player runs without a user interface.
1952
2177
  * @throws The errors of {@link Panel.ready} (rejects) The mount failed.
1953
2178
  *
1954
- * @example A toggle command for a sidebar panel (the playlist's F6)
2179
+ * @example Toggle a sidebar panel with F6, like the playlist
2180
+ * F6 and Screen ▸ List show the List tab, or hide the sidebar when the
2181
+ * list is already in front. `toggle` reads the player's state first:
2182
+ * it hides the position only when it is shown and this panel's tab is
2183
+ * the current one, otherwise it shows the panel.
1955
2184
  * ```ts
1956
2185
  * const list = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
1957
2186
  * plugin.commands.add({
1958
2187
  * id: "list",
1959
2188
  * path: ["@screen", "List"],
1960
2189
  * bindings: ["F6"],
2190
+ * // Checked while the sidebar is shown with this list in front.
1961
2191
  * checked: () => (list.state?.preferred ?? false) && (list.state?.active ?? false),
1962
2192
  * run: () => list.toggle(),
1963
2193
  * });
@@ -1986,7 +2216,12 @@ export interface Panel extends View, Disposable {
1986
2216
  *
1987
2217
  * @param locks - The locks to change; omitted ones keep their value.
1988
2218
  *
1989
- * @example Keep the bar while a search field has focus
2219
+ * @example Keep the bar on screen while a search field has focus
2220
+ * In fullscreen the bottom bar stays visible while the user types in
2221
+ * its search field, instead of hiding when the pointer leaves it. The
2222
+ * Slint side reports the field's focus through a callback, and
2223
+ * `setInteraction` sets or clears the `focused` lock; other locks keep
2224
+ * their values.
1990
2225
  * ```ts
1991
2226
  * const panel = plugin.panels.mount("bottom", "ui/bar.slint");
1992
2227
  * panel.on("searchFocused", (focused: boolean) => panel.setInteraction({ focused }));
@@ -2028,7 +2263,12 @@ export interface Panel extends View, Disposable {
2028
2263
  * finite.
2029
2264
  * @throws The errors of {@link Panel.ready} (rejects) The mount failed.
2030
2265
  *
2031
- * @example
2266
+ * @example Open a context menu where the user right-clicked (examples/panel-menu)
2267
+ * Right-clicking the list opens a menu with "Select the first item" and
2268
+ * "Remove the selected item". The Slint file passes the pointer position
2269
+ * in window pixels with its `context-menu` callback; `showMenu` opens the
2270
+ * plugin's commands there, and picking one runs that command.
2271
+ *
2032
2272
  * {@includeCode ../../examples/panel-menu/main.ts#menu}
2033
2273
  *
2034
2274
  * @since 1.0.0
@@ -2058,12 +2298,16 @@ export interface Panel extends View, Disposable {
2058
2298
  * @returns A handle that removes the listener.
2059
2299
  *
2060
2300
  * @example Add dropped files to a list
2301
+ * Files dropped on the Files panel are added to its list. While the
2302
+ * listener exists, drops on the panel come here as one group instead of
2303
+ * going to the drop providers; after the listener returns, the SDK runs
2304
+ * an update pass and `bind` sends the longer list.
2061
2305
  * ```ts
2062
2306
  * const files: string[] = [];
2063
2307
  * const panel = plugin.panels.mount("sidebar", "ui/list.slint", { title: "Files" });
2064
2308
  * panel.bind(() => ({ rows: files }));
2065
2309
  * panel.onDrop(({ paths }) => {
2066
- * files.push(...paths);
2310
+ * files.push(...paths); // in the order the file manager gave them
2067
2311
  * });
2068
2312
  * ```
2069
2313
  *
@@ -2109,16 +2353,27 @@ export interface Drops {
2109
2353
  * Take files dropped on the video area. The first provider registered for
2110
2354
  * an extension gets it; files nobody takes are ignored.
2111
2355
  *
2112
- * Paths are grouped by extension (case-insensitive) in the order the file
2356
+ * Paths are grouped by provider (an extension, case-insensitive, belongs
2357
+ * to the provider that registered it first) in the order the file
2113
2358
  * manager gave them, one call per provider. Folders are not expanded.
2114
2359
  *
2115
2360
  * @throws `invalid` synchronously for malformed extensions.
2116
2361
  *
2117
- * @example
2362
+ * @example Load dropped subtitle files
2363
+ * Dropping .srt or .ass files on the video loads each one as a subtitle
2364
+ * track. Each extension belongs to the provider that registered it first;
2365
+ * the player groups the dropped paths by provider and calls each provider
2366
+ * once with all of its paths, so .srt and .ass files dropped together
2367
+ * arrive in one call. (A panel under the pointer that listens with
2368
+ * `onDrop` takes the whole drop first.) The plugin reads each file itself
2369
+ * and uploads its bytes with `subtitles.load`.
2118
2370
  * ```ts
2371
+ * import { readFile } from "node:fs/promises";
2372
+ * // ---cut---
2119
2373
  * plugin.drops.provide({
2120
2374
  * extensions: ["srt", "ass"],
2121
2375
  * drop: async ({ paths }) => {
2376
+ * // Only .srt and .ass files reach this provider.
2122
2377
  * for (const path of paths)
2123
2378
  * await plugin.subtitles.load(await readFile(path), {
2124
2379
  * format: path.endsWith(".ass") ? "ass" : "srt",
@@ -2164,12 +2419,16 @@ export type ThemeId = "vestra" | "vestra-light";
2164
2419
  * @capability None to read `theme`; `events` to follow changes; `ui.theme`
2165
2420
  * to change it.
2166
2421
  *
2167
- * @example A menu item that selects the light palette
2422
+ * @example Offer the light palette as a menu item
2423
+ * Settings ▸ Light colours switches every view of the player to the light
2424
+ * palette and carries a radio mark while it is in use. `set` asks the
2425
+ * player to switch (it saves the choice); `theme` holds the palette in use,
2426
+ * and `checked` reads it again after every handler.
2168
2427
  * ```ts
2169
2428
  * plugin.commands.add({
2170
2429
  * id: "light-colours",
2171
2430
  * path: ["@settings", "Light colours"],
2172
- * radio: true,
2431
+ * radio: true, // a round mark instead of a check mark
2173
2432
  * checked: () => plugin.appearance.theme === "vestra-light",
2174
2433
  * run: () => plugin.appearance.set("vestra-light"),
2175
2434
  * });
@@ -2200,10 +2459,16 @@ export interface Appearance {
2200
2459
  * @param listener - Receives the new palette.
2201
2460
  * @returns A handle that removes the listener.
2202
2461
  *
2203
- * @example
2462
+ * @example Redraw a picture the plugin renders when the palette changes
2463
+ * The plugin renders a chart image in the palette's colours and draws it
2464
+ * again when the user or another plugin switches palettes. Views that use
2465
+ * `Theme` follow on their own; this listener (which needs `events`) is
2466
+ * for what the plugin draws itself.
2204
2467
  * ```ts
2468
+ * declare function renderChart(dark: boolean): Promise<void>;
2469
+ * // ---cut---
2205
2470
  * plugin.appearance.onChange((theme) => {
2206
- * void plugin.osd.show(`Palette: ${theme}`);
2471
+ * void renderChart(theme === "vestra"); // the dark default
2207
2472
  * });
2208
2473
  * ```
2209
2474
  *
@@ -2226,7 +2491,11 @@ export interface Appearance {
2226
2491
  * @throws `invalid` (rejects) `theme` is not a {@link ThemeId}.
2227
2492
  * @throws `capability` (rejects) The manifest does not declare `ui.theme`.
2228
2493
  *
2229
- * @example
2494
+ * @example Switch to the light palette
2495
+ * Every view of the player turns light, and stays light after a restart.
2496
+ * The player applies the palette to every open view and saves the
2497
+ * choice; when the promise resolves, `appearance.theme` already has the
2498
+ * new value. Without `ui.theme` it rejects with `capability`.
2230
2499
  * ```ts
2231
2500
  * await plugin.appearance.set("vestra-light");
2232
2501
  * ```
@@ -2267,10 +2536,14 @@ export interface Appearance {
2267
2536
  * @throws `capability` (rejects) The manifest does not declare `ui.theme`.
2268
2537
  * @throws `unsupported` (rejects) A player before 0.4.3.
2269
2538
  *
2270
- * @example
2539
+ * @example Choose German for the next start
2540
+ * The player will start in German next time, and the user is told so.
2541
+ * `setLanguage` only saves the choice: the language of this run
2542
+ * (`i18n.locale`) never changes while the player runs. Players before
2543
+ * 0.4.3 reject it with `unsupported`.
2271
2544
  * ```ts
2272
2545
  * await plugin.appearance.setLanguage("de");
2273
- * await plugin.osd.show("Takes effect after restart");
2546
+ * await plugin.osd.show("Takes effect after restart"); // translated by the player
2274
2547
  * ```
2275
2548
  *
2276
2549
  * @since 1.5.9
@@ -2417,6 +2690,10 @@ export type TextValue = string | Text | TextList;
2417
2690
  * @capability None.
2418
2691
  *
2419
2692
  * @example Format a date the way the interface language writes it
2693
+ * The OSD shows today's date in the user's language and date format. The
2694
+ * sentence is a template the player translates; only the data in it, the
2695
+ * date, is formatted by the plugin, with `i18n.locale` (the language the
2696
+ * player runs in, fixed for the whole run).
2420
2697
  * ```ts
2421
2698
  * const day = new Date().toLocaleDateString(plugin.i18n.locale);
2422
2699
  * await plugin.osd.show("Today is {}", [day]);
@@ -2450,17 +2727,25 @@ export interface I18n {
2450
2727
  * @param args - Values for its placeholders.
2451
2728
  * @throws `invalid` A malformed or over-limit value (`TEXT_LIMITS`).
2452
2729
  *
2453
- * @example A ComboBox model with fixed words and data
2730
+ * @example Fill a drop-down with fixed words and data
2731
+ * The drop-down lists "No Audio" and one "Track 7 · en" entry per track,
2732
+ * in the user's language. A `[string]` model may hold `text()` values: the
2733
+ * player translates each source text with the plugin's catalog and fills
2734
+ * in its arguments, so the plugin never composes translated strings.
2454
2735
  * ```ts
2455
2736
  * const { text } = plugin.i18n;
2456
2737
  * const picker = plugin.windows.create("ui/clip.slint");
2457
2738
  * const tracks = [{ id: 7, title: "en" }];
2739
+ * // Each entry is translated, then filled in: "Track 7 · en".
2458
2740
  * picker.set({
2459
2741
  * labels: [text("No Audio"), ...tracks.map((t) => text("Track {} · {}", [t.id, t.title]))],
2460
2742
  * });
2461
2743
  * ```
2462
2744
  *
2463
- * @example A message built from translated parts
2745
+ * @example Build a message from translated parts
2746
+ * The OSD says "Download failed: Connection timed out", both parts in the
2747
+ * user's language. An argument made with `text()` is translated too,
2748
+ * while string arguments are shown as they are.
2464
2749
  * ```ts
2465
2750
  * await plugin.osd.show("Download failed: {}", [plugin.i18n.text("Connection timed out")]);
2466
2751
  * ```
@@ -2480,7 +2765,11 @@ export interface I18n {
2480
2765
  * @param style - `"list"` by default.
2481
2766
  * @throws `invalid` A malformed or over-limit value.
2482
2767
  *
2483
- * @example
2768
+ * @example List file names the way the language joins them
2769
+ * The OSD says "Removed missing shaders: a.glsl, b.glsl" in English and
2770
+ * joins the names with "、" in Chinese. `list` makes a value the player
2771
+ * joins per interface language; the names themselves are data and stay
2772
+ * as they are.
2484
2773
  * ```ts
2485
2774
  * const missing = ["a.glsl", "b.glsl"];
2486
2775
  * await plugin.osd.show("Removed missing shaders: {}", [plugin.i18n.list(missing)]);
@@ -2504,13 +2793,25 @@ export interface I18n {
2504
2793
  * @param args - Values for its placeholders.
2505
2794
  * @throws `invalid` A malformed or over-limit value.
2506
2795
  *
2507
- * @example
2796
+ * @example Fail an open with a message the user can read
2797
+ * Opening a `.ch8` file the plugin cannot load shows "The program file
2798
+ * was not found." in the user's language. The player shows the message of
2799
+ * an error thrown from `resolve`; `i18n.error` makes an `Error` whose text
2800
+ * the player translates with this plugin's catalog, while its `message`
2801
+ * stays English for logs and older players.
2508
2802
  * ```ts
2803
+ * import { readFile } from "node:fs/promises";
2804
+ * declare function startEmulator(program: Uint8Array): ResolvedStream;
2805
+ * // ---cut---
2509
2806
  * plugin.resolvers.add({
2510
- * id: "nes",
2511
- * extensions: ["nes"],
2512
- * async resolve() {
2513
- * throw plugin.i18n.error("The ROM file was not found.");
2807
+ * id: "chip8",
2808
+ * extensions: ["ch8"],
2809
+ * async resolve(path, signal) {
2810
+ * const program = await readFile(path, { signal }).catch(() => {
2811
+ * // Shown as the open's failure, translated (locales/<lang>.po).
2812
+ * throw plugin.i18n.error("The program file was not found.");
2813
+ * });
2814
+ * return startEmulator(program);
2514
2815
  * },
2515
2816
  * });
2516
2817
  * ```
@@ -2536,7 +2837,11 @@ export interface I18n {
2536
2837
  * @returns The translated string.
2537
2838
  * @throws `invalid` A malformed value.
2538
2839
  *
2539
- * @example
2840
+ * @example Copy version information in the user's language
2841
+ * "Copy info" puts two translated lines on the clipboard. Text that
2842
+ * leaves the player cannot be translated by it, so `resolve` turns the
2843
+ * values into the string the player would show, using this plugin's
2844
+ * catalog; inside the player, pass the values themselves instead.
2540
2845
  * ```ts
2541
2846
  * const { text, list, resolve } = plugin.i18n;
2542
2847
  * const lines = list([text("Version {}", ["1.0"]), text("Cores: {}", [8])], "lines");
@@ -2589,7 +2894,12 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
2589
2894
  *
2590
2895
  * @capability ui.settings
2591
2896
  *
2592
- * @example Two preferences on a page, kept in a store
2897
+ * @example Put two preferences on a page and keep them in a store
2898
+ * Options ▸ Video ▸ Blur shows two check boxes whose values survive
2899
+ * restarts. The checkboxes are bound two-way (`<=>`) to `in-out`
2900
+ * properties of the bridge global, so the user's clicks show at once; the
2901
+ * `toggled` callbacks tell the plugin, which saves the new value, and
2902
+ * `bind` writes the stored values when the page opens.
2593
2903
  * ```ts
2594
2904
  * const store = plugin.storage.json("blur", {
2595
2905
  * defaults: { restoreOnStart: false, messages: true },
@@ -2603,6 +2913,7 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
2603
2913
  * restoreOnStart: store.value.restoreOnStart,
2604
2914
  * messages: store.value.messages,
2605
2915
  * }));
2916
+ * // The bridge's `restore-toggled(bool)` callback, in camelCase.
2606
2917
  * page.on("restoreToggled", (on: boolean) =>
2607
2918
  * store.save({ ...store.value, restoreOnStart: on }),
2608
2919
  * );
@@ -2617,6 +2928,7 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
2617
2928
  * import { CheckBox } from "std-widgets.slint";
2618
2929
  * import { Theme } from "@vestra/theme.slint";
2619
2930
  *
2931
+ * // The bridge: what the plugin writes and the callbacks it handles.
2620
2932
  * export global BlurPage {
2621
2933
  * in-out property <bool> restore-on-start;
2622
2934
  * in-out property <bool> messages: true;
@@ -2687,7 +2999,11 @@ export type SettingsPage = View & Disposable & {
2687
2999
  *
2688
3000
  * @capability ui.settings
2689
3001
  *
2690
- * @example A page that edits stored settings
3002
+ * @example Add a page that edits a stored setting
3003
+ * Options ▸ Playback ▸ Resume shows the plugin's Resume switch. `page`
3004
+ * registers the page with the Options window (compiled by the player on
3005
+ * its own), `bind` sends the stored value, and the page's callback saves
3006
+ * the user's change.
2691
3007
  * ```ts
2692
3008
  * const store = plugin.storage.json("settings", { defaults: { resume: true } });
2693
3009
  * const page = plugin.settings.page("ui/settings.slint", {
@@ -2696,6 +3012,7 @@ export type SettingsPage = View & Disposable & {
2696
3012
  * title: "Resume",
2697
3013
  * });
2698
3014
  * page.bind(() => ({ resume: store.value.resume }));
3015
+ * // The bridge's `resume-toggled(bool)`: save, and `bind` sends it back.
2699
3016
  * page.on("resumeToggled", (on: boolean) => store.save({ ...store.value, resume: on }));
2700
3017
  * ```
2701
3018
  *
@@ -2732,13 +3049,17 @@ export interface Settings {
2732
3049
  * one component and one global. A file that cannot be read throws Node's
2733
3050
  * file system error (`code` `ENOENT` and so on).
2734
3051
  *
2735
- * @example
3052
+ * @example Register a page and report when the player refuses it
3053
+ * An "My plugin" page appears under Plugins, or the reason it could not
3054
+ * be added is logged. `page` returns at once; the player compiles the
3055
+ * page on its own, and `ready` rejects (for example with `ui-prepare`
3056
+ * and the compiler's diagnostics) when it refuses it.
2736
3057
  * ```ts
2737
3058
  * const page = plugin.settings.page("ui/advanced.slint", {
2738
3059
  * id: "advanced",
2739
3060
  * category: "plugins",
2740
3061
  * title: "My plugin",
2741
- * order: 10,
3062
+ * order: 10, // after pages with a smaller order in the category
2742
3063
  * });
2743
3064
  * page.ready.catch((error: PluginError) => console.error(error.message));
2744
3065
  * ```
@@ -2776,7 +3097,10 @@ export interface Settings {
2776
3097
  * @throws `capability` (rejects) The manifest does not declare
2777
3098
  * `ui.settings`.
2778
3099
  *
2779
- * @example An "Options…" button in a tool window
3100
+ * @example Open the plugin's page from an "Options…" button
3101
+ * The button in the plugin's window opens the Options window at the
3102
+ * plugin's Blur page, or brings it to the front. `show` takes the id the
3103
+ * page was registered with and rejects with `invalid` for any other.
2780
3104
  * ```ts
2781
3105
  * const controls = plugin.windows.create("ui/window.slint");
2782
3106
  * controls.on("openOptions", () => plugin.settings.show("blur"));
@@ -2800,8 +3124,13 @@ export interface Settings {
2800
3124
  * @capability ui.mount; `events` to follow changes.
2801
3125
  *
2802
3126
  * @example React when a video picture appears
3127
+ * The OSD says "Video is playing" when the backdrop gives way to a video
3128
+ * picture. The player decides when the backdrop shows (nothing open,
3129
+ * loading, audio without a picture); `effective` turns false when a video
3130
+ * frame replaces it.
2803
3131
  * ```ts
2804
3132
  * plugin.panels.backdrop.onChange((state) => {
3133
+ * // `null`: no backdrop is mounted.
2805
3134
  * if (state?.effective === false) void plugin.osd.show("Video is playing");
2806
3135
  * });
2807
3136
  * ```
@@ -2827,7 +3156,10 @@ export interface BackdropSlot {
2827
3156
  * @param listener - Receives the new state (`null`: no backdrop).
2828
3157
  * @returns A handle that removes the listener.
2829
3158
  *
2830
- * @example
3159
+ * @example Log when the backdrop shows or hides
3160
+ * The plugin's log records each change of the backdrop position. The
3161
+ * listener runs when the player starts or stops showing the backdrop
3162
+ * and with `null` when it is unmounted.
2831
3163
  * ```ts
2832
3164
  * plugin.panels.backdrop.onChange((state) => console.log("backdrop", state));
2833
3165
  * ```
@@ -2863,7 +3195,13 @@ export interface BackdropSlot {
2863
3195
  *
2864
3196
  * @capability ui.mount
2865
3197
  *
2866
- * @example A sidebar list (examples/panel-menu)
3198
+ * @example Mount a list in the sidebar (examples/panel-menu)
3199
+ * The sidebar gets an "Example list" tab showing three rows. `mount`
3200
+ * returns at once; the SDK sends the mount with the values bound so far,
3201
+ * and the player rebuilds its main window with the panel. The Slint file
3202
+ * exports the component and the bridge global whose properties `bind`
3203
+ * writes.
3204
+ *
2867
3205
  * {@includeCode ../../examples/panel-menu/main.ts#mount}
2868
3206
  *
2869
3207
  * `ui/panel.slint`:
@@ -2925,9 +3263,15 @@ export interface Panels {
2925
3263
  * @throws The errors of {@link Panel.ready} (rejects `ready`) The player
2926
3264
  * refused the mount.
2927
3265
  *
2928
- * @example A bottom bar
3266
+ * @example Mount a bottom bar with a seek slider
3267
+ * The plugin's own control bar appears under the video, its slider
3268
+ * following the playback position and seeking when dragged. Bindings and
3269
+ * handlers made right after `mount` are part of the first values;
3270
+ * `ready` resolves once the main window shows the bar, and rejects with
3271
+ * `conflict` when another plugin's panel holds the bottom bar.
2929
3272
  * ```ts
2930
3273
  * const bar = plugin.panels.mount("bottom", "ui/bar.slint");
3274
+ * // Sent again whenever the position changes.
2931
3275
  * bar.bind(() => ({ position: plugin.player.state.position ?? 0 }));
2932
3276
  * bar.on("seek", (seconds: number) => plugin.player.seek(seconds));
2933
3277
  * await bar.ready;
@@ -2976,10 +3320,16 @@ export interface TimelineLayer extends Disposable {
2976
3320
  * the player translates (`text("Chapter {}", [n])`, SDK 1.5.11); other
2977
3321
  * plugins read the shown string.
2978
3322
  *
2979
- * @example
3323
+ * @example Show the file's bookmarks on the timeline
3324
+ * Bookmark marks appear on the seek bar at their times, titled with the
3325
+ * user's notes. `set` replaces this plugin's whole layer; with `ifCurrent`
3326
+ * the player drops the write if another file opened since `seen` was
3327
+ * read, so bookmarks of the previous file never land on the new one.
2980
3328
  * ```ts
3329
+ * declare const bookmarks: { time: number; note: string }[];
3330
+ * // ---cut---
2981
3331
  * const layer = plugin.timeline.layer();
2982
- * const seen = plugin.player.state;
3332
+ * const seen = plugin.player.state; // the file the bookmarks belong to
2983
3333
  * layer.set(
2984
3334
  * bookmarks.map((b) => ({ time: b.time, kind: "bookmark", title: b.note })),
2985
3335
  * { ifCurrent: seen },
@@ -3036,7 +3386,13 @@ export interface WindowState {
3036
3386
  * `onChange`; without it, it updates only at start and after this plugin's
3037
3387
  * `setVideoArea`.
3038
3388
  *
3039
- * @example A "1:1 size" command
3389
+ * @example Resize the window to the video's own size
3390
+ * Screen ▸ Original size makes the video area as many logical points as
3391
+ * the video has pixels (one pixel per pixel at a display scale of 100 %).
3392
+ * `stats()` reports the video's size;
3393
+ * `setVideoArea` resizes the window around the video column (bars and a
3394
+ * docked sidebar added), scales it down if it would not fit the screen,
3395
+ * and is refused in fullscreen, hence the `enabled` check.
3040
3396
  * ```ts
3041
3397
  * plugin.commands.add({
3042
3398
  * id: "size-1x",
@@ -3044,6 +3400,7 @@ export interface WindowState {
3044
3400
  * enabled: () => !plugin.window.state.fullscreen,
3045
3401
  * run: async () => {
3046
3402
  * const stats = await plugin.player.stats();
3403
+ * // No video stream (an audio file): nothing to size to.
3047
3404
  * if (stats.video?.width && stats.video.height)
3048
3405
  * await plugin.window.setVideoArea({ width: stats.video.width, height: stats.video.height });
3049
3406
  * },
@@ -3101,7 +3458,11 @@ export interface MainWindow {
3101
3458
  * Where it is unsupported (Windows, Linux), `setVideoArea` plus
3102
3459
  * `setAlwaysOnTop` make a reasonable "mini player" fallback.
3103
3460
  *
3104
- * @example
3461
+ * @example Toggle picture-in-picture where the system has it
3462
+ * The video moves into a floating picture-in-picture window, or back.
3463
+ * `state.pictureInPicture` says whether the system supports it and
3464
+ * whether it is on; the plugin only calls `setPictureInPicture` where it
3465
+ * is supported.
3105
3466
  * ```ts
3106
3467
  * if (plugin.window.state.pictureInPicture.supported)
3107
3468
  * await plugin.window.setPictureInPicture(!plugin.window.state.pictureInPicture.active);
@@ -3129,7 +3490,12 @@ export interface AppInfo {
3129
3490
  * `plugin.app`: the fixed facts plus the system, read on request. No
3130
3491
  * capability.
3131
3492
  *
3132
- * @example
3493
+ * @example Describe the player and the system for a bug report
3494
+ * The plugin builds a line such as "Vestra 0.5.10 on macOS 26.2" to copy
3495
+ * into a bug report. `app.version` is known from the start; each
3496
+ * `system()` call asks the player, which reads the displays at that moment.
3497
+ * The operating system's name and version it works out once, at the first
3498
+ * call, and reuses them.
3133
3499
  * ```ts
3134
3500
  * const system = await plugin.app.system();
3135
3501
  * const text = `Vestra ${plugin.app.version} on ${system.name} ${system.version ?? ""}`;
@@ -3228,18 +3594,28 @@ export interface ConfirmOptions {
3228
3594
  * at a time: another call meanwhile rejects with `busy`. Playback continues
3229
3595
  * while a dialog is open; command key bindings pause.
3230
3596
  *
3597
+ * **Platforms:** {@link Dialogs.confirm} needs macOS or Windows (other
3598
+ * systems reject it with `unsupported`); the file dialogs work on every
3599
+ * system.
3600
+ *
3231
3601
  * For anything more than a question or a path, use a {@link Windows | tool
3232
3602
  * window}.
3233
3603
  *
3234
3604
  * @capability ui.dialog
3235
3605
  *
3236
3606
  * @example Ask before deleting, remembering "Don't ask again"
3607
+ * Deleting asks for confirmation in a system alert with a "Don't ask
3608
+ * again" box; once the user ticks it, later deletes go ahead without
3609
+ * asking. `confirm` resolves with the button and the checkbox when the
3610
+ * user answers; the texts are templates the player translates, so the
3611
+ * count goes in `args`. Remembering the box is up to the plugin.
3237
3612
  * ```ts
3238
3613
  * const store = plugin.storage.json("confirm", { defaults: { askBeforeDelete: true } });
3239
3614
  * async function deleteAll(count: number) {
3240
3615
  * if (store.value.askBeforeDelete) {
3241
3616
  * const { confirmed, checked } = await plugin.dialogs.confirm({
3242
- * message: `Delete ${count} bookmarks?`,
3617
+ * message: "Delete {} bookmarks?",
3618
+ * args: [count],
3243
3619
  * detail: "This cannot be undone.",
3244
3620
  * confirm: "Delete",
3245
3621
  * destructive: true,
@@ -3286,13 +3662,22 @@ export interface Dialogs {
3286
3662
  * @throws `unsupported` (rejects) A system other than macOS and Windows.
3287
3663
  * @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
3288
3664
  *
3289
- * @example
3665
+ * @example Confirm before replacing the playlist
3666
+ * The user sees "Replace the current playlist?" with a Replace button and
3667
+ * the playlist is replaced only after a yes. The system draws the alert
3668
+ * and the promise resolves when the user answers; only one dialog is
3669
+ * open at a time, so a second one meanwhile rejects with `busy`.
3290
3670
  * ```ts
3291
- * const { confirmed } = await plugin.dialogs.confirm({
3292
- * message: "Replace the current playlist?",
3293
- * confirm: "Replace",
3294
- * });
3295
- * if (confirmed) await plugin.osd.show("Playlist replaced");
3671
+ * try {
3672
+ * const { confirmed } = await plugin.dialogs.confirm({
3673
+ * message: "Replace the current playlist?",
3674
+ * confirm: "Replace",
3675
+ * });
3676
+ * if (confirmed) await plugin.osd.show("Playlist replaced");
3677
+ * } catch (error) {
3678
+ * if ((error as PluginError).code !== "busy") throw error;
3679
+ * // Another dialog is already open: let the user finish that one.
3680
+ * }
3296
3681
  * ```
3297
3682
  *
3298
3683
  * @since 1.0.0
@@ -3316,10 +3701,13 @@ export interface Dialogs {
3316
3701
  * @throws `invalid` (rejects) `title` is longer than 256 bytes.
3317
3702
  * @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
3318
3703
  *
3319
- * @example
3704
+ * @example Let the user pick a subtitle file
3705
+ * The system's Open dialog appears titled "Choose a subtitle file" and
3706
+ * the chosen path is shown on the OSD. The promise resolves when the
3707
+ * user answers: with the absolute path, or `null` when they cancelled.
3320
3708
  * ```ts
3321
3709
  * const path = await plugin.dialogs.openFile({ title: "Choose a subtitle file" });
3322
- * if (path !== null) await plugin.osd.show(`Chosen: ${path}`);
3710
+ * if (path !== null) await plugin.osd.show("Chosen: {}", [path]); // a path is data
3323
3711
  * ```
3324
3712
  *
3325
3713
  * @since 1.0.0
@@ -3341,10 +3729,13 @@ export interface Dialogs {
3341
3729
  * @throws `invalid` (rejects) `title` is longer than 256 bytes.
3342
3730
  * @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
3343
3731
  *
3344
- * @example
3732
+ * @example Let the user pick several files at once
3733
+ * The Open dialog allows selecting several files; the OSD says how many
3734
+ * were added. The promise resolves with the absolute paths, or with an
3735
+ * empty array when the user cancelled.
3345
3736
  * ```ts
3346
3737
  * const paths = await plugin.dialogs.openFiles({ title: "Add to the playlist" });
3347
- * await plugin.osd.show(`${paths.length} files added`);
3738
+ * if (paths.length) await plugin.osd.show("{} files added", [paths.length]);
3348
3739
  * ```
3349
3740
  *
3350
3741
  * @since 1.0.0
@@ -3366,7 +3757,10 @@ export interface Dialogs {
3366
3757
  * @throws `invalid` (rejects) `title` is longer than 256 bytes.
3367
3758
  * @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
3368
3759
  *
3369
- * @example
3760
+ * @example Let the user choose a folder for snapshots
3761
+ * The system's folder dialog appears titled "Save snapshots in". It
3762
+ * resolves with the folder's absolute path, or `null` when the user
3763
+ * cancelled, in which case the plugin stops there.
3370
3764
  * ```ts
3371
3765
  * const folder = await plugin.dialogs.openDirectory({ title: "Save snapshots in" });
3372
3766
  * if (folder === null) return; // cancelled
@@ -3397,7 +3791,10 @@ export interface Dialogs {
3397
3791
  * or contains `/` or `\`.
3398
3792
  * @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
3399
3793
  *
3400
- * @example
3794
+ * @example Ask where to save a snapshot
3795
+ * The system's Save dialog proposes "frame.png" and returns where the
3796
+ * user wants it. The dialog only returns the path (or `null` when
3797
+ * cancelled); writing the file is up to the plugin afterwards.
3401
3798
  * ```ts
3402
3799
  * const path = await plugin.dialogs.saveFile({ title: "Save snapshot", fileName: "frame.png" });
3403
3800
  * if (path === null) return; // cancelled
@@ -3540,10 +3937,23 @@ export interface Chapter {
3540
3937
  * consecutive calls do not make each other stale. {@link Player.open} is
3541
3938
  * unconditional unless `ifCurrent` is given.
3542
3939
  *
3543
- * @example
3940
+ * @example Play the next file only if nothing else was opened
3941
+ * When the plugin's file ends, the next one starts, unless the user or
3942
+ * another plugin opened something in the meantime. `onEnd` passes the
3943
+ * state at the end; `open` with `ifCurrent: ended` is refused with `stale`
3944
+ * when the source changed after that state, so nothing is replaced.
3544
3945
  * ```ts
3545
- * // Advance only if nobody changed the source since it ended.
3546
- * media.onEnd((ended) => plugin.player.open(next, { ifCurrent: ended }));
3946
+ * declare const media: Media;
3947
+ * declare const next: string;
3948
+ * // ---cut---
3949
+ * media.onEnd(async (ended) => {
3950
+ * try {
3951
+ * await plugin.player.open(next, { ifCurrent: ended });
3952
+ * } catch (error) {
3953
+ * // Someone opened another file: leave it playing.
3954
+ * if ((error as PluginError).code !== "stale") throw error;
3955
+ * }
3956
+ * });
3547
3957
  * ```
3548
3958
  */
3549
3959
  export interface Conditional {
@@ -3572,10 +3982,18 @@ export interface Media {
3572
3982
  * Any outside change (another plugin opens a file, the user pauses) ends
3573
3983
  * the gesture.
3574
3984
  *
3575
- * @example
3985
+ * @example Seek while the user drags a slider
3986
+ * The video follows the slider while it is dragged. `scrub()` starts a
3987
+ * gesture from the current state; each `seek` is conditional on the
3988
+ * previous one and only the newest target is sent while one is running,
3989
+ * so a fast drag does not queue seeks. A change from outside (another file,
3990
+ * the user pausing) ends the gesture and later seeks resolve `false`.
3576
3991
  * ```ts
3992
+ * declare const bar: Panel;
3993
+ * // ---cut---
3577
3994
  * let scrub: Scrub | null = null;
3578
3995
  * bar.on("dragStart", () => (scrub = plugin.player.scrub()));
3996
+ * // Latest value wins: superseded targets resolve false, not an error.
3579
3997
  * bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds));
3580
3998
  * bar.on("dragEnd", () => scrub?.end());
3581
3999
  * ```
@@ -3599,8 +4017,15 @@ export interface Scrub {
3599
4017
  * as before. Older players ignore the option and seek exactly: set
3600
4018
  * `"minPlayerVersion": "0.5.3"` when you rely on it.
3601
4019
  *
3602
- * @example
4020
+ * @example Scrub on keyframes for a fast picture
4021
+ * While dragging, the picture jumps to the nearest earlier keyframe at
4022
+ * once instead of decoding up to the exact frame. With `keyframes: true`
4023
+ * the player lands each target on the last keyframe at or before it; the
4024
+ * plugin follows the user's keyframe-seek setting.
3603
4025
  * ```ts
4026
+ * declare const bar: Panel;
4027
+ * declare const scrub: Scrub | null;
4028
+ * // ---cut---
3604
4029
  * const keyframes = true; // the plugin's own setting
3605
4030
  * bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds, { keyframes }));
3606
4031
  * ```
@@ -3796,12 +4221,17 @@ export interface PlaybackStats {
3796
4221
  * `stale` and do nothing when the source changed since the state you saw.
3797
4222
  *
3798
4223
  * @example Open a file and wait until it plays
4224
+ * The file opens and the plugin learns whether it started playing within
4225
+ * ten seconds. `open` resolves once the player accepted the load and
4226
+ * returns a `Media` handle; `ready` waits for playback and rejects with
4227
+ * `failed` (load error), `replaced` (another source took over) or an
4228
+ * `AbortError` when the timeout fires, which closes the media.
3799
4229
  * ```ts
3800
- * const media = await plugin.player.open("/Movies/clip.mp4");
4230
+ * const media = await plugin.player.open("/Movies/clip.mp4"); // load accepted
3801
4231
  * try {
3802
4232
  * await media.ready({ signal: AbortSignal.timeout(10_000) });
3803
4233
  * } catch (error) {
3804
- * void plugin.osd.show(`Cannot play: ${(error as Error).message}`);
4234
+ * void plugin.osd.show("Cannot play: {}", [(error as Error).message]);
3805
4235
  * }
3806
4236
  * ```
3807
4237
  */
@@ -3840,15 +4270,24 @@ export interface Player {
3840
4270
  * (`timeout`), `stream`, `device`, or `replaced` when another source
3841
4271
  * replaced it (or it was closed) before it ran.
3842
4272
  *
3843
- * @example
4273
+ * @example Play pictures the plugin draws itself
4274
+ * The player shows a 320×240 picture the plugin renders, with silence as
4275
+ * its sound, as the current source. The SDK pulls frames on the audio
4276
+ * clock: each `frame()` returns one picture (RGBA bytes) and the audio
4277
+ * that plays with it, here 1/60 s, so the picture runs at 60 frames a
4278
+ * second; `frame()` may run several times back to back to refill the
4279
+ * lead. `onStop` tells the plugin when and why the stream ended.
3844
4280
  * ```ts
3845
- * const frame = new Uint8Array(320 * 240 * 4);
3846
- * const silence = new Float32Array(2 * 800); // 1/60 s of stereo
4281
+ * declare function render(into: Uint8Array): Uint8Array;
4282
+ * // ---cut---
4283
+ * const frame = new Uint8Array(320 * 240 * 4); // RGBA, reused per frame
4284
+ * const silence = new Float32Array(2 * 800); // 1/60 s of stereo at 48 kHz
3847
4285
  * const stream = await plugin.player.openStream({
3848
4286
  * width: 320,
3849
4287
  * height: 240,
3850
4288
  * frame: () => ({ video: render(frame), audio: silence }),
3851
4289
  * });
4290
+ * // `error` is null when the stream stopped normally.
3852
4291
  * stream.onStop((error) => error && plugin.osd.show(error.message));
3853
4292
  * ```
3854
4293
  */
@@ -3878,8 +4317,21 @@ export interface Player {
3878
4317
  * Become the sequence provider (one at a time; another provider rejects with
3879
4318
  * `conflict` through the scope's `onError`, or fails activation). Dispose to stop.
3880
4319
  *
3881
- * @example
4320
+ * @example Drive the player's Next and Previous from a playlist
4321
+ * The player's Next and Previous items and keys play the plugin's list.
4322
+ * The player calls `next` and `previous` when the user asks; `canNext`
4323
+ * and `canPrevious` are evaluated again after every handler the SDK runs,
4324
+ * and while they return false the player disables its items ("No next
4325
+ * item").
3882
4326
  * ```ts
4327
+ * declare const items: string[];
4328
+ * let index = 0;
4329
+ * // ---cut---
4330
+ * const playAt = async (at: number) => {
4331
+ * index = at;
4332
+ * await plugin.player.open(items[at]!);
4333
+ * };
4334
+ * // Called by the player's Next / Previous items, keys and buttons.
3883
4335
  * plugin.player.provideSequence({
3884
4336
  * next: () => playAt(index + 1),
3885
4337
  * previous: () => playAt(index - 1),
@@ -4013,9 +4465,22 @@ export interface Host {
4013
4465
  *
4014
4466
  * Prefer this over reimplementing playback buttons.
4015
4467
  *
4016
- * @example
4468
+ * @example Make a bar button play and pause like Space
4469
+ * The plugin's own Play button behaves exactly like the player's Space
4470
+ * key, including its on-screen text. `host.run` runs the core action
4471
+ * with the player's checks; it resolves when the action finished, and
4472
+ * rejects with `unavailable` while the action is disabled (nothing open).
4017
4473
  * ```ts
4018
- * bar.on("playPause", () => plugin.host.run("play-pause"));
4474
+ * declare const bar: Panel;
4475
+ * // ---cut---
4476
+ * bar.on("playPause", async () => {
4477
+ * try {
4478
+ * await plugin.host.run("play-pause");
4479
+ * } catch (error) {
4480
+ * if ((error as PluginError).code !== "unavailable") throw error;
4481
+ * // Nothing to play: the button simply does nothing.
4482
+ * }
4483
+ * });
4019
4484
  * ```
4020
4485
  */
4021
4486
  run(action: HostAction): Promise<void>;
@@ -4036,8 +4501,14 @@ export interface Host {
4036
4501
  * `seconds` and seek by the `setStepSeek` step)
4037
4502
  * @since 1.5.19
4038
4503
  *
4039
- * @example
4504
+ * @example Add a 30-second skip that adds up with the arrow keys
4505
+ * The plugin's "+30 s" button seeks forward 30 seconds and shows the same
4506
+ * text as →. Passing `seconds` makes the core step seek use that step
4507
+ * with the player's mode (exact or keyframes); quick presses of it and of
4508
+ * → add up into one seek.
4040
4509
  * ```ts
4510
+ * declare const bar: Panel;
4511
+ * // ---cut---
4041
4512
  * bar.on("forward30", () => plugin.host.run("seek-forward", { seconds: 30 }));
4042
4513
  * ```
4043
4514
  */
@@ -4058,11 +4529,16 @@ export interface Host {
4058
4529
  *
4059
4530
  * @capability ui.osd
4060
4531
  *
4061
- * @example Live playback numbers
4532
+ * @example Show live playback numbers over the video
4533
+ * A block over the video shows the position and the duration, updated
4534
+ * as playback moves. `player.onChange` runs after the SDK updated the
4535
+ * state (with `events`); `set` never throws and sends only the newest
4536
+ * text while one is in flight, so calling it on every change is fine.
4062
4537
  * ```ts
4063
4538
  * const info = plugin.osd.panel();
4064
4539
  * plugin.player.onChange((state) =>
4065
- * info.set(`${state.position?.toFixed(1) ?? "–"} / ${state.duration?.toFixed(1) ?? "–"} s`),
4540
+ * // A template with data arguments; the player translates "{} / {} s".
4541
+ * info.set("{} / {} s", [state.position?.toFixed(1) ?? "–", state.duration?.toFixed(1) ?? "–"]),
4066
4542
  * );
4067
4543
  * ```
4068
4544
  *
@@ -4089,7 +4565,10 @@ export interface OsdPanel extends Disposable {
4089
4565
  * omitted for none.
4090
4566
  * @param options - `maxColumns`: cut long lines (SDK 1.5.11).
4091
4567
  *
4092
- * @example
4568
+ * @example Show two loop points, then hide the block
4569
+ * The block shows "A: 12.0 s" and "B: 18.5 s" on two lines, then
4570
+ * disappears. The text is a template the player translates and fills
4571
+ * in; `\n` separates lines, and `null` hides the block.
4093
4572
  * ```ts
4094
4573
  * const info = plugin.osd.panel();
4095
4574
  * info.set("A: {} s\nB: {} s", ["12.0", "18.5"]);
@@ -4148,7 +4627,10 @@ export interface OsdPanelOptions {
4148
4627
  *
4149
4628
  * @capability ui.osd
4150
4629
  *
4151
- * @example On-screen messages that the user can turn off
4630
+ * @example Show on-screen messages the user can turn off
4631
+ * "Blur on" appears over the video unless the user turned messages off in
4632
+ * the plugin's settings. `osd.show` asks the player to draw the text for a
4633
+ * moment; the plugin checks its own stored preference before each call.
4152
4634
  * ```ts
4153
4635
  * const store = plugin.storage.json("prefs", { defaults: { messages: true } });
4154
4636
  * const say = (text: string) => {
@@ -4192,7 +4674,11 @@ export interface Osd {
4192
4674
  * @throws `unavailable` (rejects) The player runs without a user interface.
4193
4675
  * @throws `capability` (rejects) The manifest does not declare `ui.osd`.
4194
4676
  *
4195
- * @example
4677
+ * @example Confirm an action and show a value
4678
+ * The OSD shows "Bookmark added", then "Speed 1.5×" in the user's
4679
+ * language. The text is English source the player translates with the
4680
+ * plugin's catalog; `{}` is filled in with the argument, which stays as
4681
+ * it is because it is data.
4196
4682
  * ```ts
4197
4683
  * await plugin.osd.show("Bookmark added");
4198
4684
  * await plugin.osd.show("Speed {}×", [plugin.player.state.rate]);
@@ -4214,7 +4700,10 @@ export interface Osd {
4214
4700
  *
4215
4701
  * @throws `Error` without a code (throws) This plugin already has a block.
4216
4702
  *
4217
- * @example
4703
+ * @example Show a status block over the video
4704
+ * A small block over the video says "Loop: off" until the plugin changes
4705
+ * or hides it. Each plugin has one block, stacked with other plugins'
4706
+ * blocks; a second `panel()` call throws.
4218
4707
  * ```ts
4219
4708
  * const info = plugin.osd.panel();
4220
4709
  * info.set("Loop: off");
@@ -4292,9 +4781,14 @@ interface Filter<T> extends Disposable {
4292
4781
  * Values out of range fail with `invalid` (see {@link VideoNode}). The
4293
4782
  * same chain applies to processed snapshots and exports.
4294
4783
  *
4295
- * @example
4784
+ * @example Brighten the picture and report when it fails
4785
+ * Every video plays a little brighter with more contrast, and the OSD
4786
+ * says why when the player cannot apply it. `set` records the desired
4787
+ * chain (last write wins, one request in flight); the player reapplies it
4788
+ * for each new source, and `onStatus` reports `applied` or `failed`.
4296
4789
  * ```ts
4297
4790
  * const picture = plugin.video.filter("picture");
4791
+ * // `unsupported` while a generated stream plays: its frames skip filters.
4298
4792
  * picture.onStatus((s) => s.state === "failed" && plugin.osd.show(s.error.message));
4299
4793
  * picture.set([{ kind: "eq", brightness: 0.1, contrast: 1.1, saturation: 1, gamma: 1 }]);
4300
4794
  * ```
@@ -4309,10 +4803,13 @@ export type VideoFilter = Filter<VideoNode[]>;
4309
4803
  * `delayMs` is −10000–10000 in multiples of 10; the positive and the
4310
4804
  * negative delays of all plugins each add up to at most 10000.
4311
4805
  *
4312
- * @example
4806
+ * @example Lower the volume and delay the sound
4807
+ * Every file plays 6 dB quieter, with its sound 120 ms later to match a
4808
+ * slow display. `set` sends the plugin's whole chain and delay; the
4809
+ * player applies it to each source and adds the delays of all plugins.
4313
4810
  * ```ts
4314
4811
  * const audio = plugin.audio.filter("sound");
4315
- * audio.set({ nodes: [{ kind: "volume", gainDb: -6 }], delayMs: 120 });
4812
+ * audio.set({ nodes: [{ kind: "volume", gainDb: -6 }], delayMs: 120 }); // 10 ms steps
4316
4813
  * ```
4317
4814
  */
4318
4815
  export type AudioFilter = Filter<{
@@ -4347,7 +4844,11 @@ export interface ViewSettings {
4347
4844
  * Out-of-range values or unknown fields fail with `invalid` without
4348
4845
  * sending anything.
4349
4846
  *
4350
- * @example
4847
+ * @example Fill the window, then zoom in
4848
+ * The video fills the video area (cropped instead of letterboxed), then
4849
+ * zooms to 150 % shifted a little to the right. The first plugin to call
4850
+ * `view()` owns the display layer; each `set` merges into its settings,
4851
+ * which stay across sources until the plugin resets them or stops.
4351
4852
  * ```ts
4352
4853
  * const view = plugin.video.view();
4353
4854
  * view.set({ fit: "fill" });
@@ -4388,13 +4889,20 @@ export interface RenderSettings {
4388
4889
  * `unsupported`. macOS OpenGL 4.1 has no compute shaders, so `peakDetect`
4389
4890
  * is accepted but has no effect; output is always SDR.
4390
4891
  *
4391
- * @example
4892
+ * @example Tone-map HDR video and hide the menu where it cannot work
4893
+ * On a GPU-rendering player HDR video is tone-mapped with BT.2390 for a
4894
+ * 250-nit display; under software rendering the plugin hides its HDR menu.
4895
+ * `set` merges the values into the plugin's render options and the
4896
+ * player reports the result through `onStatus`: `failed` with
4897
+ * `unsupported` means the player renders in software (Windows, Linux).
4392
4898
  * ```ts
4899
+ * declare function hideHdrMenu(): void;
4900
+ * // ---cut---
4393
4901
  * const render = plugin.video.render();
4394
4902
  * render.onStatus((s) => {
4395
4903
  * if (s.state === "failed" && s.error.code === "unsupported") hideHdrMenu();
4396
4904
  * });
4397
- * render.set({ toneMapping: "bt.2390", targetPeak: 250 });
4905
+ * render.set({ toneMapping: "bt.2390", targetPeak: 250 }); // nits
4398
4906
  * ```
4399
4907
  */
4400
4908
  export interface RenderOptions extends Disposable {
@@ -4455,7 +4963,10 @@ export interface SubtitlesState {
4455
4963
  /** Font family name for display. */
4456
4964
  family: string;
4457
4965
  }[];
4458
- /** The effective style, from whichever plugin set it last. */
4966
+ /**
4967
+ * The effective style: among the plugins that set a layer, the one whose
4968
+ * manifest id sorts last (not the most recent call); `null` without one.
4969
+ */
4459
4970
  readonly style: SubtitleStyle | null;
4460
4971
  /** Whether the renderer confirmed the selection. */
4461
4972
  readonly trackConfirmation: "waiting" | "applied" | "uncertain";
@@ -4475,12 +4986,23 @@ export interface SubtitleConditional {
4475
4986
  * `subtitles.read`, since they read the current selection first.
4476
4987
  *
4477
4988
  * @example Load an SRT file as the secondary subtitle
4989
+ * An English SRT file shows as a second subtitle line next to the
4990
+ * film's own subtitles, in white with an outline. The plugin reads the
4991
+ * file and uploads its bytes with `load`, which adds a track to the
4992
+ * current source and selects it as the secondary subtitle; `refresh`
4993
+ * reads the subtitle state for its font list, and `setStyle` sends this
4994
+ * plugin's style layer naming one of those fonts.
4478
4995
  * ```ts
4996
+ * import { readFile } from "node:fs/promises";
4997
+ * declare const path: string;
4998
+ * // ---cut---
4479
4999
  * const bytes = await readFile(path);
4480
- * const track = await plugin.subtitles.load(bytes, { title: "English", select: "secondary" });
5000
+ * await plugin.subtitles.load(bytes, { title: "English", select: "secondary" });
5001
+ * const { fonts } = await plugin.subtitles.refresh();
5002
+ * if (!fonts.length) return;
4481
5003
  * await plugin.subtitles.setStyle({
4482
- * fontId: plugin.subtitles.state!.fonts[0]!.id,
4483
- * fontSize: 40,
5004
+ * fontId: fonts[0]!.id,
5005
+ * fontSize: 40, // at the 1080p reference height
4484
5006
  * color: "white",
4485
5007
  * outline: 2,
4486
5008
  * marginPercent: 5,
@@ -4524,7 +5046,12 @@ export interface Subtitles {
4524
5046
  }): Promise<SubtitleTrack>;
4525
5047
  /** Remove a track this plugin loaded. */
4526
5048
  remove(track: SubtitleTrack, options?: SubtitleConditional): Promise<SubtitlesState>;
4527
- /** This plugin's style layer; `null` removes it. Last write wins. */
5049
+ /**
5050
+ * Set this plugin's style layer; `null` removes it. The newest call of
5051
+ * this plugin replaces its earlier layer. When several plugins have a
5052
+ * layer, the player uses the one whose manifest id sorts last, whatever
5053
+ * the order of the calls.
5054
+ */
4528
5055
  setStyle(style: SubtitleStyle | null, options?: SubtitleConditional): Promise<SubtitlesState>;
4529
5056
  }
4530
5057
  /**
@@ -4627,11 +5154,17 @@ export interface ExportJob extends Disposable {
4627
5154
  * `subtitles.read`). Exports decode offline and do not disturb playback.
4628
5155
  *
4629
5156
  * @example Save the current frame as JPEG
5157
+ * The frame on screen, with the plugin's own picture effects, is saved
5158
+ * where the user chooses. `capture` freezes the source as it was in
5159
+ * `seen` (and fails with `stale` if the user opened another file while
5160
+ * the dialog was open); `export` decodes offline without disturbing
5161
+ * playback, and `done` resolves with the job's final status.
4630
5162
  * ```ts
4631
5163
  * const seen = plugin.player.state;
4632
5164
  * const destination = await plugin.dialogs.saveFile({ fileName: "frame.jpg" });
4633
5165
  * if (!destination) return;
4634
5166
  * const snapshot = await plugin.media.capture({ ifCurrent: seen, processed: true });
5167
+ * // Release the frozen frame whatever happens to the export.
4635
5168
  * try {
4636
5169
  * const job = await snapshot.export({ format: "jpeg", destination, quality: 85 });
4637
5170
  * const end = await job.done;
@@ -4691,23 +5224,28 @@ export interface WebResolverOptions {
4691
5224
  * (the player shows a warning). When this plugin stops, its extensions are
4692
5225
  * free again and such files go to the player as before.
4693
5226
  *
4694
- * @example
5227
+ * @example Play CHIP-8 programs with the plugin's own emulator
5228
+ * Opening a `.ch8` file anywhere in the player (Open dialog, playlist,
5229
+ * recent list) runs it in the plugin's emulator instead of failing as an
5230
+ * unknown format. The player calls `resolve` with the path; the plugin
5231
+ * reads the program and returns a stream whose `frame()` the SDK pulls on
5232
+ * the audio clock, one 1/60 s frame per call. `started` runs once the
5233
+ * stream plays; `signal` aborts the read when the open is replaced or
5234
+ * closed.
4695
5235
  * ```ts
4696
- * const { readFile } = await import("node:fs/promises");
4697
- * // The plugin's own emulator: one frame per call.
4698
- * const emulator = (rom: Uint8Array): (() => StreamFrame) => {
4699
- * const video = new Uint8Array(256 * 240 * 4).fill(rom[0] ?? 0);
4700
- * return () => ({ video, audio: new Float32Array(1600) });
4701
- * };
5236
+ * import { readFile } from "node:fs/promises";
5237
+ * // The plugin's own emulator (made up here): one frame per call.
5238
+ * declare function emulator(program: Uint8Array): () => StreamFrame;
5239
+ * // ---cut---
4702
5240
  * plugin.resolvers.add({
4703
- * id: "nes",
4704
- * extensions: ["nes"],
5241
+ * id: "chip8",
5242
+ * extensions: ["ch8"], // lower case, no dot; matches Game.CH8 too
4705
5243
  * async resolve(path, signal) {
4706
5244
  * const frame = emulator(await readFile(path, { signal }));
4707
5245
  * return {
4708
5246
  * kind: "stream",
4709
- * width: 256,
4710
- * height: 240,
5247
+ * width: 64, // CHIP-8's display
5248
+ * height: 32,
4711
5249
  * frame,
4712
5250
  * started: (stream) => stream.onStop((error) => error && plugin.osd.show(error.message)),
4713
5251
  * };
@@ -4772,14 +5310,20 @@ export interface Resolver extends Disposable {
4772
5310
  * files into streams this plugin generates. Needs `source.resolve` (file
4773
5311
  * resolvers also `producer.av`).
4774
5312
  *
4775
- * @example
5313
+ * @example Play pages of a video site
5314
+ * Opening a `https://video.example.com/…` page plays its video. When the
5315
+ * user opens a URL whose scheme and host this resolver declares, the
5316
+ * player asks the plugin first; `resolve` returns the stream's address,
5317
+ * and `signal` aborts the lookup when the open is replaced or closed.
4776
5318
  * ```ts
4777
5319
  * plugin.resolvers.add({
4778
5320
  * id: "example",
4779
5321
  * schemes: ["https"],
4780
5322
  * hosts: ["video.example.com"],
4781
5323
  * async resolve(url, signal) {
4782
- * const page = await (await fetch(url, { signal })).json();
5324
+ * const response = await fetch(url, { signal }); // the site's JSON for the page
5325
+ * if (!response.ok) throw new Error(`video.example.com answered ${response.status}`);
5326
+ * const page = await response.json();
4783
5327
  * return { kind: "media", originalUrl: url, title: page.title, primary: { url: page.stream } };
4784
5328
  * },
4785
5329
  * });
@@ -4802,9 +5346,13 @@ export interface Resolvers {
4802
5346
  * Use it to accept such files beside the player's own media types, for
4803
5347
  * example in a playlist's drop or folder scan.
4804
5348
  *
4805
- * @example
5349
+ * @example Accept files that a resolver plugin can play
5350
+ * A plugin's own file filter also accepts types other plugins turn into
5351
+ * streams. `fileTypes` lists the extensions every running file resolver
5352
+ * claims (lower case, no dot); the SDK keeps it current with `events`.
4806
5353
  * ```ts
4807
5354
  * const MEDIA = ["mp4", "mkv", "mp3"];
5355
+ * // Extensions compare in lower case, without the dot.
4808
5356
  * const accepted = (path: string) => {
4809
5357
  * const extension = path.split(".").pop()?.toLowerCase() ?? "";
4810
5358
  * return MEDIA.includes(extension) || plugin.resolvers.fileTypes.includes(extension);
@@ -4823,10 +5371,13 @@ export interface Resolvers {
4823
5371
  * @param listener - Receives the new list.
4824
5372
  * @returns A handle that removes the listener.
4825
5373
  *
4826
- * @example
5374
+ * @example Say which file types became playable
5375
+ * When a plugin with a file resolver starts or stops, the OSD lists the
5376
+ * extra file types now accepted. The listener (which needs `events`)
5377
+ * receives the new list each time it changes.
4827
5378
  * ```ts
4828
5379
  * plugin.resolvers.onFileTypesChange((types) => {
4829
- * void plugin.osd.show(`Also accepted: ${types.join(", ")}`);
5380
+ * void plugin.osd.show("Also accepted: {}", [plugin.i18n.list(types)]);
4830
5381
  * });
4831
5382
  * ```
4832
5383
  *
@@ -5049,10 +5600,19 @@ export interface PluginWarning {
5049
5600
  * `plugin.packages`: the plugins the player knows. `list` and `onChange`
5050
5601
  * need no capability; everything else needs `plugins.manage`.
5051
5602
  *
5052
- * @example
5603
+ * @example Install a plugin from an unpacked package
5604
+ * The plugin installs a package directory it downloaded and unpacked, and
5605
+ * reports why when it does not work. `validate` asks the player to check
5606
+ * the package and returns a candidate; `install` starts the change and
5607
+ * `done` resolves with its final status: `committed` only once the new
5608
+ * plugin reported that it is running, otherwise `failed`, `cancelled`,
5609
+ * `failedDegraded` or `tooLate`.
5053
5610
  * ```ts
5611
+ * declare const unpackedDirectory: string;
5612
+ * // ---cut---
5054
5613
  * const candidate = await plugin.packages.validate(unpackedDirectory);
5055
5614
  * const change = await candidate.install();
5615
+ * // `message` says why a change did not commit.
5056
5616
  * if ((await change.done) !== "committed") throw new Error(change.message ?? "install failed");
5057
5617
  * ```
5058
5618
  */
@@ -5165,7 +5725,11 @@ export interface BindingInfo extends BindingRef {
5165
5725
  * `plugin.bindings`: list and override every command's bindings, including
5166
5726
  * the player's. Needs `bindings.manage`.
5167
5727
  *
5168
- * @example "Ignore the default shortcut" for one command
5728
+ * @example Unbind the player's Mute key, then restore it
5729
+ * The player's Mute command loses its default key, and later gets it
5730
+ * back. `set` with `[]` saves an override that unbinds the command
5731
+ * (remembered across launches); `reset` removes the override, so the
5732
+ * command's own default bindings apply again.
5169
5733
  * ```ts
5170
5734
  * await plugin.bindings.set({ plugin: "host", command: "mute" }, []);
5171
5735
  * // later
@@ -5205,9 +5769,18 @@ export interface Bindings {
5205
5769
  * PNG. Other systems: `unsupported`. Failures: `invalid`, `missing`,
5206
5770
  * `unavailable`, `failed` (the system refused).
5207
5771
  *
5208
- * @example
5772
+ * @example Copy the current file and position
5773
+ * The clipboard holds "path @ seconds" for pasting into a note. The
5774
+ * player writes the system clipboard; it rejects with `unsupported` on
5775
+ * systems other than macOS and Windows and `failed` when the system
5776
+ * refuses, which the plugin reports.
5209
5777
  * ```ts
5210
- * await plugin.clipboard.writeText(`${plugin.player.state.source} @ ${plugin.player.state.position}`);
5778
+ * const { source, position } = plugin.player.state;
5779
+ * try {
5780
+ * await plugin.clipboard.writeText(`${source} @ ${position}`); // data, not shown
5781
+ * } catch (error) {
5782
+ * void plugin.osd.show("Could not copy: {}", [(error as PluginError).code]);
5783
+ * }
5211
5784
  * ```
5212
5785
  */
5213
5786
  export interface Clipboard {
@@ -5243,15 +5816,23 @@ export interface ToolResult {
5243
5816
  * `plugin.tools`: ffmpeg and ffprobe, run in this plugin's process. No
5244
5817
  * capability.
5245
5818
  *
5246
- * @example
5819
+ * @example Read a file's format with ffprobe
5820
+ * The plugin gets a media file's container details as JSON. `available`
5821
+ * says whether ffprobe can be found (`VESTRA_FFPROBE`, then `PATH`); `run`
5822
+ * starts it from the plugin's own process without a shell and resolves
5823
+ * with its output as bytes once it exits, or rejects when it fails
5824
+ * (`failed`, with the last stderr line) or takes longer than `timeoutMs`.
5247
5825
  * ```ts
5826
+ * declare const path: string;
5827
+ * // ---cut---
5248
5828
  * if (await plugin.tools.available("ffprobe")) {
5249
5829
  * const { stdout } = await plugin.tools.run(
5250
5830
  * "ffprobe",
5251
5831
  * ["-v", "error", "-show_format", "-of", "json", path],
5252
- * { timeoutMs: 10_000 },
5832
+ * { timeoutMs: 10_000 }, // give up on a stuck file after 10 s
5253
5833
  * );
5254
5834
  * const info = JSON.parse(new TextDecoder().decode(stdout));
5835
+ * void info.format; // container, duration, bit rate…
5255
5836
  * }
5256
5837
  * ```
5257
5838
  */
@@ -5298,14 +5879,22 @@ export interface Storage {
5298
5879
  * The file is read synchronously on creation; `debounceMs` defaults to
5299
5880
  * 300. Files are created with mode 0600. `invalid` for a bad name.
5300
5881
  *
5301
- * @example
5882
+ * @example Keep settings and a recent list in a JSON file
5883
+ * The plugin remembers whether to resume and the last ten files across
5884
+ * restarts. `json` reads `settings.json` from the data directory at once
5885
+ * (or uses `defaults` when it is missing or fails `validate`); `save`
5886
+ * replaces the value and writes the file 300 ms later, and stopping the
5887
+ * plugin writes a pending save first.
5302
5888
  * ```ts
5889
+ * declare const path: string;
5890
+ * // ---cut---
5303
5891
  * type Settings = { resume: boolean; recent: string[] };
5304
5892
  * const store = plugin.storage.json<Settings>("settings", {
5305
5893
  * defaults: { resume: true, recent: [] },
5306
5894
  * validate: (v): v is Settings =>
5307
5895
  * typeof v === "object" && v !== null && typeof (v as Settings).resume === "boolean",
5308
5896
  * });
5897
+ * // Only `save` writes: changing `store.value` in place is not saved.
5309
5898
  * store.save({ ...store.value, recent: [path, ...store.value.recent].slice(0, 10) });
5310
5899
  * ```
5311
5900
  */
@@ -5367,7 +5956,11 @@ export interface Storage {
5367
5956
  * resolves, so a crash right after loses nothing; there is no debounce.
5368
5957
  * Calls answer in order.
5369
5958
  *
5370
- * @example
5959
+ * @example Keep a work queue across a plugin restart
5960
+ * A download queue survives the plugin being reloaded or crashing, and is
5961
+ * empty again when the player starts next time. Session storage lives in
5962
+ * the player process for one player run; each `set` is kept once its
5963
+ * promise resolves, so nothing is lost if the plugin dies right after.
5371
5964
  * ```ts
5372
5965
  * // A queue the plugin is working through: after a reload or a crash it
5373
5966
  * // goes on where it stopped, after the player quits it starts afresh.
@@ -5396,10 +5989,21 @@ export interface SessionStorage {
5396
5989
  * `plugin.system`: files and links, executed by the player. No capability.
5397
5990
  * Other systems than macOS and Windows: `unsupported`.
5398
5991
  *
5399
- * @example
5992
+ * @example Show an exported file and open the help page
5993
+ * The file manager opens with the exported file selected, then the
5994
+ * browser opens the plugin's help page. The player performs both on the
5995
+ * plugin's behalf; they reject with `unsupported` on systems other than
5996
+ * macOS and Windows, which the plugin turns into a message.
5400
5997
  * ```ts
5401
- * await plugin.system.reveal(exportedFile);
5402
- * await plugin.system.openUrl("https://example.com/help");
5998
+ * declare const exportedFile: string;
5999
+ * // ---cut---
6000
+ * try {
6001
+ * await plugin.system.reveal(exportedFile);
6002
+ * await plugin.system.openUrl("https://example.com/help");
6003
+ * } catch (error) {
6004
+ * if ((error as PluginError).code !== "unsupported") throw error;
6005
+ * await plugin.osd.show(`Saved to ${exportedFile}`); // no file manager to open
6006
+ * }
5403
6007
  * ```
5404
6008
  */
5405
6009
  export interface System {
@@ -5427,12 +6031,25 @@ export interface System {
5427
6031
  * The user denying access and other system errors reject `failed`. Never
5428
6032
  * write secrets to settings files.
5429
6033
  *
5430
- * @example
6034
+ * @example Ask for an API token once and keep it in the keychain
6035
+ * The user types the token the first time; later runs read it from the
6036
+ * system's secret store. `get` resolves `null` when nothing is stored; a
6037
+ * store that is missing or refuses access rejects (`unsupported`,
6038
+ * `unavailable`, `failed`), and then the plugin keeps the token for this
6039
+ * run only and asks again next time.
5431
6040
  * ```ts
5432
- * let token = await plugin.secrets.get("api-token");
6041
+ * declare function askUser(): Promise<string | null>;
6042
+ * // ---cut---
6043
+ * let token: string | null = null;
6044
+ * try {
6045
+ * token = await plugin.secrets.get("api-token");
6046
+ * } catch {
6047
+ * // No usable store: fall through and ask.
6048
+ * }
5433
6049
  * if (token === null) {
5434
6050
  * token = await askUser();
5435
- * await plugin.secrets.set("api-token", token);
6051
+ * if (token)
6052
+ * await plugin.secrets.set("api-token", token).catch(() => {}); // best effort
5436
6053
  * }
5437
6054
  * ```
5438
6055
  */