@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.
- package/CHANGELOG.md +12 -0
- package/dist/capabilities.d.ts +13 -3
- package/dist/capabilities.js +13 -3
- package/dist/commands.d.ts +49 -9
- package/dist/commands.js +49 -9
- package/dist/index.d.ts +6 -1
- package/dist/palettes.d.ts +16 -2
- package/dist/palettes.js +16 -2
- package/dist/plugin/bindings.d.ts +27 -3
- package/dist/plugin/bindings.js +27 -3
- package/dist/plugin/osd.d.ts +13 -1
- package/dist/plugin/osd.js +13 -1
- package/dist/plugin/services.d.ts +46 -6
- package/dist/plugin/services.js +46 -6
- package/dist/plugin/testing.d.ts +353 -62
- package/dist/plugin/testing.js +343 -61
- package/dist/plugin/types.d.ts +781 -164
- package/dist/plugin/values.d.ts +27 -3
- package/dist/plugin/values.js +27 -3
- package/dist/processing.d.ts +16 -4
- package/dist/source-resolvers.d.ts +33 -8
- package/dist/subtitles.d.ts +19 -9
- package/dist/wire.d.ts +12 -3
- package/dist/wire.js +12 -3
- package/llms-full.txt +3312 -20
- package/llms.txt +20 -20
- package/package.json +1 -1
package/dist/plugin/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
913
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
2511
|
-
* extensions: ["
|
|
2512
|
-
* async resolve() {
|
|
2513
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
3292
|
-
*
|
|
3293
|
-
*
|
|
3294
|
-
*
|
|
3295
|
-
*
|
|
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(
|
|
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(
|
|
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
|
-
*
|
|
3546
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
3846
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
4697
|
-
* // The plugin's own emulator: one frame per call.
|
|
4698
|
-
*
|
|
4699
|
-
*
|
|
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: "
|
|
4704
|
-
* extensions: ["
|
|
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:
|
|
4710
|
-
* height:
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
5402
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
6051
|
+
* if (token)
|
|
6052
|
+
* await plugin.secrets.set("api-token", token).catch(() => {}); // best effort
|
|
5436
6053
|
* }
|
|
5437
6054
|
* ```
|
|
5438
6055
|
*/
|