@vestra-player/sdk 1.5.28 → 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 +23 -0
- package/bin/vestra-plugin.mjs +103 -0
- package/dist/capabilities.d.ts +13 -3
- package/dist/capabilities.js +13 -3
- package/dist/commands.d.ts +94 -8
- package/dist/commands.js +124 -7
- package/dist/index.d.ts +6 -1
- package/dist/palettes.d.ts +16 -2
- package/dist/palettes.js +16 -2
- package/dist/player-runtime.d.ts +1 -1
- package/dist/player-runtime.js +1 -1
- package/dist/plugin/bindings.d.ts +27 -3
- package/dist/plugin/bindings.js +27 -3
- package/dist/plugin/commands.js +29 -2
- 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 +403 -61
- package/dist/plugin/testing.js +408 -60
- package/dist/plugin/types.d.ts +860 -162
- 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 +3503 -25
- package/llms.txt +26 -26
- package/package.json +1 -1
- package/slint/vestra/bar-buttons.slint +138 -0
- package/slint/vestra/icons.slint +4 -0
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",
|
|
@@ -386,6 +413,82 @@ export type Bindable<T> = T | (() => T);
|
|
|
386
413
|
* `MENU_IDS` in `@vestra-player/sdk/testing`.
|
|
387
414
|
*/
|
|
388
415
|
export type MenuId = "open" | "playback" | "subtitles" | "video" | "audio" | "screen" | "settings";
|
|
416
|
+
/**
|
|
417
|
+
* An icon of the player's V-Line set (`@vestra/icons.slint`), by name: what
|
|
418
|
+
* a bottom-bar button may show. The list is `BAR_BUTTON_ICONS` in
|
|
419
|
+
* `@vestra-player/sdk/testing`.
|
|
420
|
+
*
|
|
421
|
+
* @since 1.5.29
|
|
422
|
+
*/
|
|
423
|
+
export type BarButtonIcon = "play" | "pause" | "stop" | "back" | "forward" | "open" | "volume" | "mute" | "playlist" | "detach" | "attach" | "close" | "failed" | "move-top" | "move-up" | "move-down" | "move-bottom" | "search" | "chapter" | "bookmark" | "check-task" | "check-box" | "menu-check" | "submenu" | "scroll-up" | "scroll-down" | "caret-down" | "sort-asc" | "sort-desc" | "tag-remove" | "preview-play" | "add" | "remove" | "sort" | "fullscreen" | "exit-fullscreen" | "store" | "ai";
|
|
424
|
+
/**
|
|
425
|
+
* A command's button on the bottom bar: {@link CommandOptions.button}.
|
|
426
|
+
*
|
|
427
|
+
* @remarks
|
|
428
|
+
* **Where it shows.** The bottom bar places the player's strip of command
|
|
429
|
+
* buttons, `BarButtons` from `@vestra/bar-buttons.slint`, at its right end;
|
|
430
|
+
* the player fills it with the buttons of every plugin, left to right by
|
|
431
|
+
* `order`, and its own sidebar toggle rightmost. When the window is narrow
|
|
432
|
+
* the strip loses cells from its outer (left) end inward: the lowest order
|
|
433
|
+
* goes first, the sidebar toggle last. A
|
|
434
|
+
* bottom bar that does not place `BarButtons` shows no buttons: the
|
|
435
|
+
* command's menu entry stays, and the player logs the buttons it cannot
|
|
436
|
+
* show and whose bar hides them. The built-in bar (`vestra.playback`) places
|
|
437
|
+
* it; see
|
|
438
|
+
* [Writing a bottom bar](../../docs/guidelines/interface.md#writing-a-bottom-bar).
|
|
439
|
+
*
|
|
440
|
+
* **What it shows.** The icon, the command's title (translated, a trailing
|
|
441
|
+
* "…" removed) and its first key as the tooltip ("Plugin Store (F9)"),
|
|
442
|
+
* `enabled`, `busy` and `reason` as the menu item has them: a disabled or
|
|
443
|
+
* busy command's button is drawn disabled, its tooltip adds the reason, and
|
|
444
|
+
* a click runs nothing. A command with `checked` shows its icon in the
|
|
445
|
+
* accent colour while checked. A click runs the command exactly like a
|
|
446
|
+
* click on its menu item.
|
|
447
|
+
*
|
|
448
|
+
* **Limits.** One button per plugin and eight in the player
|
|
449
|
+
* (`BAR_BUTTON_LIMITS`): a button past them is left out (its command and
|
|
450
|
+
* menu entry stay) and the player logs it with `limit`. The button goes
|
|
451
|
+
* with its command: disposing the command, or disabling, stopping or
|
|
452
|
+
* uninstalling the plugin, removes it.
|
|
453
|
+
*
|
|
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.
|
|
460
|
+
* ```ts
|
|
461
|
+
* const store = plugin.windows.create("ui/store.slint");
|
|
462
|
+
* plugin.commands.add({
|
|
463
|
+
* id: "open",
|
|
464
|
+
* path: ["Plugin Store…"], // a button needs a menu path
|
|
465
|
+
* button: { icon: "store", order: 200 },
|
|
466
|
+
* run: () => store.show(),
|
|
467
|
+
* });
|
|
468
|
+
* ```
|
|
469
|
+
*
|
|
470
|
+
* @player 0.5.10; older players show the menu entry without a button
|
|
471
|
+
* (declare `"minPlayerVersion": "0.5.10"` when the button matters).
|
|
472
|
+
* @since 1.5.29
|
|
473
|
+
*/
|
|
474
|
+
export interface CommandButton {
|
|
475
|
+
/**
|
|
476
|
+
* A name of the player's icon set ({@link BarButtonIcon}), or an image
|
|
477
|
+
* made with {@link Ui.image} (a PNG or JPEG in your package or data
|
|
478
|
+
* directory), drawn in the 14×12 icon box, aspect kept, coloured like the
|
|
479
|
+
* player's line icons: only its alpha counts, so draw it as a dark shape
|
|
480
|
+
* on a transparent background. An unknown name fails with `invalid`.
|
|
481
|
+
*/
|
|
482
|
+
icon: BarButtonIcon | ImageRef;
|
|
483
|
+
/**
|
|
484
|
+
* Place in the strip, left to right ascending: an integer 0–999. It is
|
|
485
|
+
* position only: a lower order sits further left, and in a narrow window
|
|
486
|
+
* (or past the limit of eight) it is the first to go. Equal orders sort
|
|
487
|
+
* by plugin id, then declaration order. The player's own buttons use AI
|
|
488
|
+
* 100, Plugin Store 200.
|
|
489
|
+
*/
|
|
490
|
+
order: number;
|
|
491
|
+
}
|
|
389
492
|
/**
|
|
390
493
|
* The declaration passed to {@link Commands.add}.
|
|
391
494
|
*
|
|
@@ -444,10 +547,19 @@ export interface CommandOptions {
|
|
|
444
547
|
* ({})", [n, codec])`. The menu shows it translated and filled in; it
|
|
445
548
|
* merges by its English rendering ("Track 2 (opus)").
|
|
446
549
|
*
|
|
447
|
-
* @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.
|
|
448
556
|
* ```ts
|
|
557
|
+
* declare function toggle(): void;
|
|
558
|
+
* declare function select(): void;
|
|
559
|
+
* // ---cut---
|
|
449
560
|
* plugin.commands.add({ id: "blur", path: ["@video", "Blur"], run: toggle });
|
|
450
561
|
* const { text } = plugin.i18n;
|
|
562
|
+
* // A segment with data: translated as "Track {} ({})", then filled in.
|
|
451
563
|
* plugin.commands.add({
|
|
452
564
|
* id: "track-2",
|
|
453
565
|
* path: ["@audio", "Audio Track", text("Track {} ({})", [2, "opus"])],
|
|
@@ -500,6 +612,16 @@ export interface CommandOptions {
|
|
|
500
612
|
focus?: "panel";
|
|
501
613
|
/** Fire again on keyboard auto-repeat while the key is held. Default false. */
|
|
502
614
|
repeat?: boolean;
|
|
615
|
+
/**
|
|
616
|
+
* A button for this command on the bottom bar (SDK 1.5.29, player 0.5.10):
|
|
617
|
+
* an icon and a place, `{ icon: "store", order: 200 }`. Needs a `path`: the
|
|
618
|
+
* menu entry is where the command stays reachable when the bar shows no
|
|
619
|
+
* button (a bar without `BarButtons`, a narrow window, a limit). See
|
|
620
|
+
* {@link CommandButton} for where it shows, what it shows and the limits.
|
|
621
|
+
* Fails with `invalid` for an unknown icon name, an order outside 0–999 or
|
|
622
|
+
* a button without a path.
|
|
623
|
+
*/
|
|
624
|
+
button?: CommandButton;
|
|
503
625
|
/** Whether the command can run (default true). */
|
|
504
626
|
enabled?: Bindable<boolean>;
|
|
505
627
|
/** Check mark (or radio dot with `radio`); `null` (default) draws none. */
|
|
@@ -529,9 +651,26 @@ export interface Command extends Disposable {
|
|
|
529
651
|
/**
|
|
530
652
|
* Replace some state fields (values or functions). Last write wins.
|
|
531
653
|
*
|
|
532
|
-
* @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.
|
|
533
659
|
* ```ts
|
|
534
|
-
*
|
|
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
|
+
* });
|
|
535
674
|
* ```
|
|
536
675
|
*/
|
|
537
676
|
set(state: Partial<{
|
|
@@ -559,12 +698,17 @@ export interface Commands {
|
|
|
559
698
|
* unknown `@` menu id), order, group or binding; `conflict` for an id this
|
|
560
699
|
* plugin has already declared and not disposed.
|
|
561
700
|
*
|
|
562
|
-
* @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.
|
|
563
707
|
* ```ts
|
|
564
708
|
* const toggle = plugin.commands.add({
|
|
565
709
|
* id: "toggle-pause",
|
|
566
710
|
* path: ["Playback", "Pause"],
|
|
567
|
-
* order: 210,
|
|
711
|
+
* order: 210, // position among the Playback menu's items
|
|
568
712
|
* bindings: ["Ctrl+P"],
|
|
569
713
|
* enabled: () => plugin.player.state.can.pause,
|
|
570
714
|
* checked: () => plugin.player.state.paused,
|
|
@@ -582,8 +726,8 @@ export interface Commands {
|
|
|
582
726
|
* {@link View.bind}: the whole value of an `image` property or, from SDK
|
|
583
727
|
* 1.5.19 (player 0.5.1), an `image` field at any depth, such as the logo of
|
|
584
728
|
* each row of a list. Making the reference does not read the file: the
|
|
585
|
-
* player checks and decodes it each time
|
|
586
|
-
* 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).
|
|
587
731
|
*
|
|
588
732
|
* A reference belongs to the {@link Scope} that created it. Once that scope
|
|
589
733
|
* is disposed, writing the reference fails with `invalid` (reported to
|
|
@@ -591,13 +735,24 @@ export interface Commands {
|
|
|
591
735
|
* property changes or the view closes.
|
|
592
736
|
*
|
|
593
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.
|
|
594
744
|
* ```ts
|
|
595
745
|
* const cover = plugin.windows.create("ui/cover.slint");
|
|
746
|
+
* // `art` is an `image` property in ui/cover.slint.
|
|
596
747
|
* cover.set({ art: plugin.ui.image(`${plugin.dataDirectory}/cover.png`) });
|
|
597
748
|
* await cover.show();
|
|
598
749
|
* ```
|
|
599
750
|
*
|
|
600
|
-
* @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.
|
|
601
756
|
* ```slint
|
|
602
757
|
* export struct Row { key: string, name: string, icon: image }
|
|
603
758
|
* ```
|
|
@@ -608,6 +763,7 @@ export interface Commands {
|
|
|
608
763
|
* rows: items.map((item) => ({
|
|
609
764
|
* key: item.id,
|
|
610
765
|
* name: item.name,
|
|
766
|
+
* // About twice the drawn size (32 px) stays sharp on dense screens.
|
|
611
767
|
* icon: plugin.ui.image(item.iconPath, { maxSide: 64 }),
|
|
612
768
|
* })),
|
|
613
769
|
* }));
|
|
@@ -658,7 +814,11 @@ export interface ImageOptions {
|
|
|
658
814
|
* scope that created it: once that scope is disposed, writing it fails with
|
|
659
815
|
* `invalid`, wherever it is in the value.
|
|
660
816
|
*
|
|
661
|
-
* @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.
|
|
662
822
|
* ```ts
|
|
663
823
|
* const chat = plugin.windows.create("ui/chat.slint");
|
|
664
824
|
* const body = await plugin.ui.markdown("**Hello**", { remoteImages: false });
|
|
@@ -752,7 +912,11 @@ export type PropValue = PropData | ImageRef;
|
|
|
752
912
|
* JSON whose values may also be Markdown documents and (SDK 1.5.19) images,
|
|
753
913
|
* at any depth: the part of {@link PropValue} that nests.
|
|
754
914
|
*
|
|
755
|
-
* @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.
|
|
756
920
|
* ```ts
|
|
757
921
|
* const messages: PropData = [
|
|
758
922
|
* { id: "m1", role: "user", body: await plugin.ui.markdown("Hi") },
|
|
@@ -774,7 +938,11 @@ export type PropData = null | boolean | number | string | MarkdownRef | ImageRef
|
|
|
774
938
|
* kebab-case are kept. Only the property names are converted, not the
|
|
775
939
|
* fields of struct values.
|
|
776
940
|
*
|
|
777
|
-
* @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.
|
|
778
946
|
* ```ts
|
|
779
947
|
* const props: Props = { canMark: true, rows: [{ rowId: "a", title: "First" }] };
|
|
780
948
|
* ```
|
|
@@ -798,7 +966,13 @@ export type Props = Record<string, PropValue>;
|
|
|
798
966
|
*
|
|
799
967
|
* @capability None.
|
|
800
968
|
*
|
|
801
|
-
* @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
|
+
*
|
|
802
976
|
* {@includeCode ../../examples/markdown/main.ts#document}
|
|
803
977
|
*
|
|
804
978
|
* `ui/document.slint`:
|
|
@@ -815,8 +989,8 @@ export interface Ui {
|
|
|
815
989
|
*
|
|
816
990
|
* @remarks
|
|
817
991
|
* Returns at once without touching the file. The player checks the file
|
|
818
|
-
* each time
|
|
819
|
-
* 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
|
|
820
994
|
* directory or {@link Plugin.dataDirectory} after following links, at most
|
|
821
995
|
* 16 MB and 4096×4096 pixels, and PNG or JPEG by content (the extension
|
|
822
996
|
* does not matter). A failed check of a whole-value image is a failed
|
|
@@ -828,9 +1002,10 @@ export interface Ui {
|
|
|
828
1002
|
* the value is shown, as a Markdown image shows its alt text.
|
|
829
1003
|
*
|
|
830
1004
|
* The player caches decoded images per plugin (up to 64 MB, least recently
|
|
831
|
-
* used first)
|
|
832
|
-
*
|
|
833
|
-
* 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.
|
|
834
1009
|
*
|
|
835
1010
|
* @capability None.
|
|
836
1011
|
*
|
|
@@ -844,6 +1019,12 @@ export interface Ui {
|
|
|
844
1019
|
* `maxSide` is not an integer 1–4096.
|
|
845
1020
|
*
|
|
846
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.
|
|
847
1028
|
* ```ts
|
|
848
1029
|
* const window = plugin.windows.create("ui/thumbnail.slint");
|
|
849
1030
|
* const file = `${plugin.dataDirectory}/thumbnail.png`;
|
|
@@ -881,7 +1062,12 @@ export interface Ui {
|
|
|
881
1062
|
* large, or is not a PNG or JPEG image; or the plugin has no data
|
|
882
1063
|
* directory.
|
|
883
1064
|
*
|
|
884
|
-
* @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.
|
|
885
1071
|
* ```ts
|
|
886
1072
|
* const results = [{ title: "A", thumbnailUrl: "https://example.com/a.jpg" }];
|
|
887
1073
|
* const placeholder = plugin.ui.image(`${plugin.dataDirectory}/blank.png`);
|
|
@@ -890,7 +1076,7 @@ export interface Ui {
|
|
|
890
1076
|
* title: r.title,
|
|
891
1077
|
* thumb: await plugin.ui
|
|
892
1078
|
* .remoteImage(r.thumbnailUrl, { maxSide: 320 })
|
|
893
|
-
* .catch(() => placeholder),
|
|
1079
|
+
* .catch(() => placeholder), // `unavailable`: download failed
|
|
894
1080
|
* })),
|
|
895
1081
|
* );
|
|
896
1082
|
* plugin.windows.create("ui/results.slint").set({ rows });
|
|
@@ -943,7 +1129,14 @@ export interface Ui {
|
|
|
943
1129
|
* `base` is neither an `http(s):` address nor an absolute path.
|
|
944
1130
|
* @throws `unsupported` (rejects) The player is older than 0.2.0.
|
|
945
1131
|
*
|
|
946
|
-
* @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
|
+
*
|
|
947
1140
|
* `ui/chat.slint`:
|
|
948
1141
|
*
|
|
949
1142
|
* ```slint
|
|
@@ -970,6 +1163,7 @@ export interface Ui {
|
|
|
970
1163
|
* const chat = plugin.windows.create("ui/chat.slint");
|
|
971
1164
|
* const history: { id: string; role: string; text: string }[] = [];
|
|
972
1165
|
* chat.on("copyCode", (code: string) => plugin.clipboard.writeText(code));
|
|
1166
|
+
* // Re-send the whole list: unchanged documents cost the player nothing.
|
|
973
1167
|
* async function render() {
|
|
974
1168
|
* const messages = [];
|
|
975
1169
|
* for (const { id, role, text } of history)
|
|
@@ -982,10 +1176,19 @@ export interface Ui {
|
|
|
982
1176
|
* ```
|
|
983
1177
|
*
|
|
984
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`.
|
|
985
1185
|
* ```ts
|
|
986
1186
|
* const readmeUrl = "https://example.com/plugin/README.md";
|
|
987
|
-
* const
|
|
1187
|
+
* const response = await fetch(readmeUrl);
|
|
1188
|
+
* if (!response.ok) throw new Error(`README: HTTP ${response.status}`);
|
|
1189
|
+
* const text = await response.text();
|
|
988
1190
|
* const window = plugin.windows.create("ui/readme.slint");
|
|
1191
|
+
* // Bound before the first `show`, as a tool window requires.
|
|
989
1192
|
* window.on("openLink", (url: string) => plugin.system.openUrl(url));
|
|
990
1193
|
* window.set({ doc: await plugin.ui.markdown(text, { base: readmeUrl }) });
|
|
991
1194
|
* await window.show();
|
|
@@ -1028,7 +1231,13 @@ export interface Ui {
|
|
|
1028
1231
|
*
|
|
1029
1232
|
* @capability That of the view: `ui.window`, `ui.mount` or `ui.settings`.
|
|
1030
1233
|
*
|
|
1031
|
-
* @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
|
+
*
|
|
1032
1241
|
* {@includeCode ../../examples/bookmark/main.ts#window}
|
|
1033
1242
|
*
|
|
1034
1243
|
* `ui/bookmark.slint`:
|
|
@@ -1073,10 +1282,16 @@ export interface View {
|
|
|
1073
1282
|
* @throws `Error` without a code (throws) On an open {@link Window}, for a
|
|
1074
1283
|
* name that has no handler yet.
|
|
1075
1284
|
*
|
|
1076
|
-
* @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`.
|
|
1077
1291
|
* ```ts
|
|
1078
1292
|
* const picture = plugin.windows.create("ui/picture.slint");
|
|
1079
1293
|
* let brightness = 0;
|
|
1294
|
+
* // camelCase here, `brightness-changed` in the Slint file.
|
|
1080
1295
|
* picture.on("brightnessChanged", (value: number) => {
|
|
1081
1296
|
* brightness = value;
|
|
1082
1297
|
* });
|
|
@@ -1115,6 +1330,10 @@ export interface View {
|
|
|
1115
1330
|
* @param props - Properties by name; see {@link Props}.
|
|
1116
1331
|
*
|
|
1117
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.
|
|
1118
1337
|
* ```ts
|
|
1119
1338
|
* const window = plugin.windows.create("ui/presets.slint");
|
|
1120
1339
|
* let presetIndex = 0;
|
|
@@ -1150,7 +1369,11 @@ export interface View {
|
|
|
1150
1369
|
* @param props - Returns the properties, by name.
|
|
1151
1370
|
* @returns A handle that stops this binding (the values last sent stay).
|
|
1152
1371
|
*
|
|
1153
|
-
* @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.
|
|
1154
1377
|
* ```ts
|
|
1155
1378
|
* const status = plugin.windows.create("ui/status.slint");
|
|
1156
1379
|
* status.bind(() => ({
|
|
@@ -1200,10 +1423,19 @@ export interface View {
|
|
|
1200
1423
|
*
|
|
1201
1424
|
* @capability ui.window
|
|
1202
1425
|
*
|
|
1203
|
-
* @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
|
+
*
|
|
1204
1432
|
* {@includeCode ../../examples/bookmark/main.ts#window,command}
|
|
1205
1433
|
*
|
|
1206
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.
|
|
1207
1439
|
* ```ts
|
|
1208
1440
|
* const clock = plugin.windows.create("ui/clock.slint");
|
|
1209
1441
|
* let timer: NodeJS.Timeout | undefined;
|
|
@@ -1217,6 +1449,7 @@ export interface View {
|
|
|
1217
1449
|
* path: ["@screen", "Clock…"],
|
|
1218
1450
|
* run: async () => {
|
|
1219
1451
|
* await clock.show();
|
|
1452
|
+
* // Re-evaluate `bind` once a second: bound values change only then.
|
|
1220
1453
|
* timer ??= setInterval(() => plugin.update(), 1000);
|
|
1221
1454
|
* },
|
|
1222
1455
|
* });
|
|
@@ -1258,12 +1491,17 @@ export interface Window extends View, Disposable {
|
|
|
1258
1491
|
* JPEG.
|
|
1259
1492
|
*
|
|
1260
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.
|
|
1261
1498
|
* ```ts
|
|
1262
1499
|
* const window = plugin.windows.create("ui/inspector.slint");
|
|
1263
1500
|
* try {
|
|
1264
1501
|
* await window.show();
|
|
1265
1502
|
* } catch (error) {
|
|
1266
|
-
*
|
|
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]);
|
|
1267
1505
|
* }
|
|
1268
1506
|
* ```
|
|
1269
1507
|
*
|
|
@@ -1285,10 +1523,14 @@ export interface Window extends View, Disposable {
|
|
|
1285
1523
|
* @throws `missing` (rejects) The user closed the window just before (the
|
|
1286
1524
|
* close had not reached the plugin yet).
|
|
1287
1525
|
*
|
|
1288
|
-
* @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.
|
|
1289
1531
|
* ```ts
|
|
1290
1532
|
* const levels = plugin.windows.create("ui/levels.slint");
|
|
1291
|
-
* let visible = false;
|
|
1533
|
+
* let visible = false; // what the check mark shows
|
|
1292
1534
|
* plugin.commands.add({
|
|
1293
1535
|
* id: "levels",
|
|
1294
1536
|
* path: ["@audio", "Levels"],
|
|
@@ -1321,6 +1563,10 @@ export interface Window extends View, Disposable {
|
|
|
1321
1563
|
* @throws `missing` (rejects) The user closed the window just before.
|
|
1322
1564
|
*
|
|
1323
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.
|
|
1324
1570
|
* ```ts
|
|
1325
1571
|
* const editor = plugin.windows.create("ui/editor.slint");
|
|
1326
1572
|
* editor.on("done", () => editor.close());
|
|
@@ -1353,7 +1599,12 @@ export interface Window extends View, Disposable {
|
|
|
1353
1599
|
* @param listener - Called with no arguments; may be async.
|
|
1354
1600
|
* @returns A handle that removes the listener.
|
|
1355
1601
|
*
|
|
1356
|
-
* @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.
|
|
1357
1608
|
* ```ts
|
|
1358
1609
|
* const store = plugin.storage.json("window", { defaults: { open: false } });
|
|
1359
1610
|
* const window = plugin.windows.create("ui/notes.slint");
|
|
@@ -1379,7 +1630,11 @@ export interface Window extends View, Disposable {
|
|
|
1379
1630
|
*
|
|
1380
1631
|
* @capability ui.window
|
|
1381
1632
|
*
|
|
1382
|
-
* @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
|
+
*
|
|
1383
1638
|
* {@includeCode ../../examples/bookmark/main.ts#window,command}
|
|
1384
1639
|
*
|
|
1385
1640
|
* @see {@link Window}, [Windows, panels, settings pages, dialogs and the OSD](../../docs/concepts/ui.md)
|
|
@@ -1414,7 +1669,10 @@ export interface Windows {
|
|
|
1414
1669
|
* @throws `conflict` (throws) This plugin already has a window with this id
|
|
1415
1670
|
* (SDK 1.3.0; before, a plain `Error` without `code`).
|
|
1416
1671
|
*
|
|
1417
|
-
* @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.
|
|
1418
1676
|
* ```ts
|
|
1419
1677
|
* const left = plugin.windows.create("ui/compare.slint", { id: "compare-left" });
|
|
1420
1678
|
* const right = plugin.windows.create("ui/compare.slint", { id: "compare-right" });
|
|
@@ -1515,12 +1773,18 @@ export interface PanelState {
|
|
|
1515
1773
|
*
|
|
1516
1774
|
* @capability ui.mount; `events` to follow changes made elsewhere.
|
|
1517
1775
|
*
|
|
1518
|
-
* @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.
|
|
1519
1782
|
* ```ts
|
|
1520
1783
|
* const sidebar = plugin.panels.sidebar;
|
|
1521
1784
|
* plugin.commands.add({
|
|
1522
1785
|
* id: "sidebar",
|
|
1523
1786
|
* path: ["Tools", "Show sidebar"],
|
|
1787
|
+
* // `state` is null while no plugin has a panel in the sidebar.
|
|
1524
1788
|
* checked: () => sidebar.state?.preferred ?? false,
|
|
1525
1789
|
* enabled: () => sidebar.state !== null,
|
|
1526
1790
|
* run: () => sidebar.toggle(),
|
|
@@ -1563,9 +1827,14 @@ export interface PanelSlot {
|
|
|
1563
1827
|
* @param listener - Receives the new state.
|
|
1564
1828
|
* @returns A handle that removes the listener.
|
|
1565
1829
|
*
|
|
1566
|
-
* @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.
|
|
1567
1835
|
* ```ts
|
|
1568
1836
|
* plugin.panels.sidebar.onChange((state) => {
|
|
1837
|
+
* // `null`: no panel is mounted in the sidebar any more.
|
|
1569
1838
|
* if (state?.reason === "space") void plugin.osd.show("Widen the window to see the playlist");
|
|
1570
1839
|
* });
|
|
1571
1840
|
* ```
|
|
@@ -1591,9 +1860,15 @@ export interface PanelSlot {
|
|
|
1591
1860
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1592
1861
|
* @throws `capability` (rejects) The manifest does not declare `ui.mount`.
|
|
1593
1862
|
*
|
|
1594
|
-
* @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.
|
|
1595
1868
|
* ```ts
|
|
1596
|
-
* 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
|
+
* });
|
|
1597
1872
|
* ```
|
|
1598
1873
|
*
|
|
1599
1874
|
* @since 1.0.0
|
|
@@ -1614,11 +1889,16 @@ export interface PanelSlot {
|
|
|
1614
1889
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1615
1890
|
* @throws `capability` (rejects) The manifest does not declare `ui.mount`.
|
|
1616
1891
|
*
|
|
1617
|
-
* @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`.
|
|
1618
1897
|
* ```ts
|
|
1619
1898
|
* plugin.commands.add({
|
|
1620
1899
|
* id: "toggle-bar",
|
|
1621
1900
|
* path: ["@screen", "Control bar"],
|
|
1901
|
+
* // The user's preference, remembered across launches.
|
|
1622
1902
|
* checked: () => plugin.panels.bottom.state?.preferred ?? false,
|
|
1623
1903
|
* run: () => plugin.panels.bottom.toggle(),
|
|
1624
1904
|
* });
|
|
@@ -1650,7 +1930,11 @@ export interface PanelSlot {
|
|
|
1650
1930
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1651
1931
|
* @throws `capability` (rejects) The manifest does not declare `ui.mount`.
|
|
1652
1932
|
*
|
|
1653
|
-
* @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.
|
|
1654
1938
|
* ```ts
|
|
1655
1939
|
* plugin.commands.add({
|
|
1656
1940
|
* id: "to-playlist",
|
|
@@ -1676,12 +1960,17 @@ export interface PanelSlot {
|
|
|
1676
1960
|
*
|
|
1677
1961
|
* @capability ui.mount
|
|
1678
1962
|
*
|
|
1679
|
-
* @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.
|
|
1680
1968
|
* ```ts
|
|
1681
1969
|
* const sidebar = plugin.panels.sidebar;
|
|
1682
1970
|
* plugin.commands.add({
|
|
1683
1971
|
* id: "detach",
|
|
1684
1972
|
* path: ["Tools", "Detach sidebar"],
|
|
1973
|
+
* // Detaching moves every tab of the sidebar into the floating window.
|
|
1685
1974
|
* checked: () => sidebar.state?.detached ?? false,
|
|
1686
1975
|
* enabled: () => sidebar.state !== null,
|
|
1687
1976
|
* run: () => sidebar.setDetached(!sidebar.state?.detached),
|
|
@@ -1704,7 +1993,10 @@ export interface SidebarSlot extends PanelSlot {
|
|
|
1704
1993
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1705
1994
|
* @throws `capability` (rejects) The manifest does not declare `ui.mount`.
|
|
1706
1995
|
*
|
|
1707
|
-
* @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.
|
|
1708
2000
|
* ```ts
|
|
1709
2001
|
* const sidebar = plugin.panels.sidebar;
|
|
1710
2002
|
* await sidebar.setDetached(!sidebar.state?.detached);
|
|
@@ -1737,7 +2029,13 @@ export interface SidebarSlot extends PanelSlot {
|
|
|
1737
2029
|
*
|
|
1738
2030
|
* @capability ui.mount
|
|
1739
2031
|
*
|
|
1740
|
-
* @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
|
+
*
|
|
1741
2039
|
* {@includeCode ../../examples/panel-menu/main.ts#mount,menu}
|
|
1742
2040
|
*
|
|
1743
2041
|
* `ui/panel.slint`:
|
|
@@ -1812,7 +2110,12 @@ export interface Panel extends View, Disposable {
|
|
|
1812
2110
|
* @param listener - Receives the new state.
|
|
1813
2111
|
* @returns A handle that removes the listener.
|
|
1814
2112
|
*
|
|
1815
|
-
* @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).
|
|
1816
2119
|
* ```ts
|
|
1817
2120
|
* const panel = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
|
|
1818
2121
|
* panel.onChange((state) => {
|
|
@@ -1842,7 +2145,10 @@ export interface Panel extends View, Disposable {
|
|
|
1842
2145
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1843
2146
|
* @throws The errors of {@link Panel.ready} (rejects) The mount failed.
|
|
1844
2147
|
*
|
|
1845
|
-
* @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`.
|
|
1846
2152
|
* ```ts
|
|
1847
2153
|
* const list = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
|
|
1848
2154
|
* plugin.commands.add({ id: "show", title: "Show the list", run: () => list.show() });
|
|
@@ -1870,13 +2176,18 @@ export interface Panel extends View, Disposable {
|
|
|
1870
2176
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1871
2177
|
* @throws The errors of {@link Panel.ready} (rejects) The mount failed.
|
|
1872
2178
|
*
|
|
1873
|
-
* @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.
|
|
1874
2184
|
* ```ts
|
|
1875
2185
|
* const list = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
|
|
1876
2186
|
* plugin.commands.add({
|
|
1877
2187
|
* id: "list",
|
|
1878
2188
|
* path: ["@screen", "List"],
|
|
1879
2189
|
* bindings: ["F6"],
|
|
2190
|
+
* // Checked while the sidebar is shown with this list in front.
|
|
1880
2191
|
* checked: () => (list.state?.preferred ?? false) && (list.state?.active ?? false),
|
|
1881
2192
|
* run: () => list.toggle(),
|
|
1882
2193
|
* });
|
|
@@ -1905,7 +2216,12 @@ export interface Panel extends View, Disposable {
|
|
|
1905
2216
|
*
|
|
1906
2217
|
* @param locks - The locks to change; omitted ones keep their value.
|
|
1907
2218
|
*
|
|
1908
|
-
* @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.
|
|
1909
2225
|
* ```ts
|
|
1910
2226
|
* const panel = plugin.panels.mount("bottom", "ui/bar.slint");
|
|
1911
2227
|
* panel.on("searchFocused", (focused: boolean) => panel.setInteraction({ focused }));
|
|
@@ -1947,7 +2263,12 @@ export interface Panel extends View, Disposable {
|
|
|
1947
2263
|
* finite.
|
|
1948
2264
|
* @throws The errors of {@link Panel.ready} (rejects) The mount failed.
|
|
1949
2265
|
*
|
|
1950
|
-
* @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
|
+
*
|
|
1951
2272
|
* {@includeCode ../../examples/panel-menu/main.ts#menu}
|
|
1952
2273
|
*
|
|
1953
2274
|
* @since 1.0.0
|
|
@@ -1977,12 +2298,16 @@ export interface Panel extends View, Disposable {
|
|
|
1977
2298
|
* @returns A handle that removes the listener.
|
|
1978
2299
|
*
|
|
1979
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.
|
|
1980
2305
|
* ```ts
|
|
1981
2306
|
* const files: string[] = [];
|
|
1982
2307
|
* const panel = plugin.panels.mount("sidebar", "ui/list.slint", { title: "Files" });
|
|
1983
2308
|
* panel.bind(() => ({ rows: files }));
|
|
1984
2309
|
* panel.onDrop(({ paths }) => {
|
|
1985
|
-
* files.push(...paths);
|
|
2310
|
+
* files.push(...paths); // in the order the file manager gave them
|
|
1986
2311
|
* });
|
|
1987
2312
|
* ```
|
|
1988
2313
|
*
|
|
@@ -2028,16 +2353,27 @@ export interface Drops {
|
|
|
2028
2353
|
* Take files dropped on the video area. The first provider registered for
|
|
2029
2354
|
* an extension gets it; files nobody takes are ignored.
|
|
2030
2355
|
*
|
|
2031
|
-
* 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
|
|
2032
2358
|
* manager gave them, one call per provider. Folders are not expanded.
|
|
2033
2359
|
*
|
|
2034
2360
|
* @throws `invalid` synchronously for malformed extensions.
|
|
2035
2361
|
*
|
|
2036
|
-
* @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`.
|
|
2037
2370
|
* ```ts
|
|
2371
|
+
* import { readFile } from "node:fs/promises";
|
|
2372
|
+
* // ---cut---
|
|
2038
2373
|
* plugin.drops.provide({
|
|
2039
2374
|
* extensions: ["srt", "ass"],
|
|
2040
2375
|
* drop: async ({ paths }) => {
|
|
2376
|
+
* // Only .srt and .ass files reach this provider.
|
|
2041
2377
|
* for (const path of paths)
|
|
2042
2378
|
* await plugin.subtitles.load(await readFile(path), {
|
|
2043
2379
|
* format: path.endsWith(".ass") ? "ass" : "srt",
|
|
@@ -2083,12 +2419,16 @@ export type ThemeId = "vestra" | "vestra-light";
|
|
|
2083
2419
|
* @capability None to read `theme`; `events` to follow changes; `ui.theme`
|
|
2084
2420
|
* to change it.
|
|
2085
2421
|
*
|
|
2086
|
-
* @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.
|
|
2087
2427
|
* ```ts
|
|
2088
2428
|
* plugin.commands.add({
|
|
2089
2429
|
* id: "light-colours",
|
|
2090
2430
|
* path: ["@settings", "Light colours"],
|
|
2091
|
-
* radio: true,
|
|
2431
|
+
* radio: true, // a round mark instead of a check mark
|
|
2092
2432
|
* checked: () => plugin.appearance.theme === "vestra-light",
|
|
2093
2433
|
* run: () => plugin.appearance.set("vestra-light"),
|
|
2094
2434
|
* });
|
|
@@ -2119,10 +2459,16 @@ export interface Appearance {
|
|
|
2119
2459
|
* @param listener - Receives the new palette.
|
|
2120
2460
|
* @returns A handle that removes the listener.
|
|
2121
2461
|
*
|
|
2122
|
-
* @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.
|
|
2123
2467
|
* ```ts
|
|
2468
|
+
* declare function renderChart(dark: boolean): Promise<void>;
|
|
2469
|
+
* // ---cut---
|
|
2124
2470
|
* plugin.appearance.onChange((theme) => {
|
|
2125
|
-
* void
|
|
2471
|
+
* void renderChart(theme === "vestra"); // the dark default
|
|
2126
2472
|
* });
|
|
2127
2473
|
* ```
|
|
2128
2474
|
*
|
|
@@ -2145,7 +2491,11 @@ export interface Appearance {
|
|
|
2145
2491
|
* @throws `invalid` (rejects) `theme` is not a {@link ThemeId}.
|
|
2146
2492
|
* @throws `capability` (rejects) The manifest does not declare `ui.theme`.
|
|
2147
2493
|
*
|
|
2148
|
-
* @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`.
|
|
2149
2499
|
* ```ts
|
|
2150
2500
|
* await plugin.appearance.set("vestra-light");
|
|
2151
2501
|
* ```
|
|
@@ -2186,10 +2536,14 @@ export interface Appearance {
|
|
|
2186
2536
|
* @throws `capability` (rejects) The manifest does not declare `ui.theme`.
|
|
2187
2537
|
* @throws `unsupported` (rejects) A player before 0.4.3.
|
|
2188
2538
|
*
|
|
2189
|
-
* @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`.
|
|
2190
2544
|
* ```ts
|
|
2191
2545
|
* await plugin.appearance.setLanguage("de");
|
|
2192
|
-
* await plugin.osd.show("Takes effect after restart");
|
|
2546
|
+
* await plugin.osd.show("Takes effect after restart"); // translated by the player
|
|
2193
2547
|
* ```
|
|
2194
2548
|
*
|
|
2195
2549
|
* @since 1.5.9
|
|
@@ -2336,6 +2690,10 @@ export type TextValue = string | Text | TextList;
|
|
|
2336
2690
|
* @capability None.
|
|
2337
2691
|
*
|
|
2338
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).
|
|
2339
2697
|
* ```ts
|
|
2340
2698
|
* const day = new Date().toLocaleDateString(plugin.i18n.locale);
|
|
2341
2699
|
* await plugin.osd.show("Today is {}", [day]);
|
|
@@ -2369,17 +2727,25 @@ export interface I18n {
|
|
|
2369
2727
|
* @param args - Values for its placeholders.
|
|
2370
2728
|
* @throws `invalid` A malformed or over-limit value (`TEXT_LIMITS`).
|
|
2371
2729
|
*
|
|
2372
|
-
* @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.
|
|
2373
2735
|
* ```ts
|
|
2374
2736
|
* const { text } = plugin.i18n;
|
|
2375
2737
|
* const picker = plugin.windows.create("ui/clip.slint");
|
|
2376
2738
|
* const tracks = [{ id: 7, title: "en" }];
|
|
2739
|
+
* // Each entry is translated, then filled in: "Track 7 · en".
|
|
2377
2740
|
* picker.set({
|
|
2378
2741
|
* labels: [text("No Audio"), ...tracks.map((t) => text("Track {} · {}", [t.id, t.title]))],
|
|
2379
2742
|
* });
|
|
2380
2743
|
* ```
|
|
2381
2744
|
*
|
|
2382
|
-
* @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.
|
|
2383
2749
|
* ```ts
|
|
2384
2750
|
* await plugin.osd.show("Download failed: {}", [plugin.i18n.text("Connection timed out")]);
|
|
2385
2751
|
* ```
|
|
@@ -2399,7 +2765,11 @@ export interface I18n {
|
|
|
2399
2765
|
* @param style - `"list"` by default.
|
|
2400
2766
|
* @throws `invalid` A malformed or over-limit value.
|
|
2401
2767
|
*
|
|
2402
|
-
* @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.
|
|
2403
2773
|
* ```ts
|
|
2404
2774
|
* const missing = ["a.glsl", "b.glsl"];
|
|
2405
2775
|
* await plugin.osd.show("Removed missing shaders: {}", [plugin.i18n.list(missing)]);
|
|
@@ -2423,13 +2793,25 @@ export interface I18n {
|
|
|
2423
2793
|
* @param args - Values for its placeholders.
|
|
2424
2794
|
* @throws `invalid` A malformed or over-limit value.
|
|
2425
2795
|
*
|
|
2426
|
-
* @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.
|
|
2427
2802
|
* ```ts
|
|
2803
|
+
* import { readFile } from "node:fs/promises";
|
|
2804
|
+
* declare function startEmulator(program: Uint8Array): ResolvedStream;
|
|
2805
|
+
* // ---cut---
|
|
2428
2806
|
* plugin.resolvers.add({
|
|
2429
|
-
* id: "
|
|
2430
|
-
* extensions: ["
|
|
2431
|
-
* async resolve() {
|
|
2432
|
-
*
|
|
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);
|
|
2433
2815
|
* },
|
|
2434
2816
|
* });
|
|
2435
2817
|
* ```
|
|
@@ -2455,7 +2837,11 @@ export interface I18n {
|
|
|
2455
2837
|
* @returns The translated string.
|
|
2456
2838
|
* @throws `invalid` A malformed value.
|
|
2457
2839
|
*
|
|
2458
|
-
* @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.
|
|
2459
2845
|
* ```ts
|
|
2460
2846
|
* const { text, list, resolve } = plugin.i18n;
|
|
2461
2847
|
* const lines = list([text("Version {}", ["1.0"]), text("Cores: {}", [8])], "lines");
|
|
@@ -2508,7 +2894,12 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
|
|
|
2508
2894
|
*
|
|
2509
2895
|
* @capability ui.settings
|
|
2510
2896
|
*
|
|
2511
|
-
* @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.
|
|
2512
2903
|
* ```ts
|
|
2513
2904
|
* const store = plugin.storage.json("blur", {
|
|
2514
2905
|
* defaults: { restoreOnStart: false, messages: true },
|
|
@@ -2522,6 +2913,7 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
|
|
|
2522
2913
|
* restoreOnStart: store.value.restoreOnStart,
|
|
2523
2914
|
* messages: store.value.messages,
|
|
2524
2915
|
* }));
|
|
2916
|
+
* // The bridge's `restore-toggled(bool)` callback, in camelCase.
|
|
2525
2917
|
* page.on("restoreToggled", (on: boolean) =>
|
|
2526
2918
|
* store.save({ ...store.value, restoreOnStart: on }),
|
|
2527
2919
|
* );
|
|
@@ -2536,6 +2928,7 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
|
|
|
2536
2928
|
* import { CheckBox } from "std-widgets.slint";
|
|
2537
2929
|
* import { Theme } from "@vestra/theme.slint";
|
|
2538
2930
|
*
|
|
2931
|
+
* // The bridge: what the plugin writes and the callbacks it handles.
|
|
2539
2932
|
* export global BlurPage {
|
|
2540
2933
|
* in-out property <bool> restore-on-start;
|
|
2541
2934
|
* in-out property <bool> messages: true;
|
|
@@ -2606,7 +2999,11 @@ export type SettingsPage = View & Disposable & {
|
|
|
2606
2999
|
*
|
|
2607
3000
|
* @capability ui.settings
|
|
2608
3001
|
*
|
|
2609
|
-
* @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.
|
|
2610
3007
|
* ```ts
|
|
2611
3008
|
* const store = plugin.storage.json("settings", { defaults: { resume: true } });
|
|
2612
3009
|
* const page = plugin.settings.page("ui/settings.slint", {
|
|
@@ -2615,6 +3012,7 @@ export type SettingsPage = View & Disposable & {
|
|
|
2615
3012
|
* title: "Resume",
|
|
2616
3013
|
* });
|
|
2617
3014
|
* page.bind(() => ({ resume: store.value.resume }));
|
|
3015
|
+
* // The bridge's `resume-toggled(bool)`: save, and `bind` sends it back.
|
|
2618
3016
|
* page.on("resumeToggled", (on: boolean) => store.save({ ...store.value, resume: on }));
|
|
2619
3017
|
* ```
|
|
2620
3018
|
*
|
|
@@ -2651,13 +3049,17 @@ export interface Settings {
|
|
|
2651
3049
|
* one component and one global. A file that cannot be read throws Node's
|
|
2652
3050
|
* file system error (`code` `ENOENT` and so on).
|
|
2653
3051
|
*
|
|
2654
|
-
* @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.
|
|
2655
3057
|
* ```ts
|
|
2656
3058
|
* const page = plugin.settings.page("ui/advanced.slint", {
|
|
2657
3059
|
* id: "advanced",
|
|
2658
3060
|
* category: "plugins",
|
|
2659
3061
|
* title: "My plugin",
|
|
2660
|
-
* order: 10,
|
|
3062
|
+
* order: 10, // after pages with a smaller order in the category
|
|
2661
3063
|
* });
|
|
2662
3064
|
* page.ready.catch((error: PluginError) => console.error(error.message));
|
|
2663
3065
|
* ```
|
|
@@ -2695,7 +3097,10 @@ export interface Settings {
|
|
|
2695
3097
|
* @throws `capability` (rejects) The manifest does not declare
|
|
2696
3098
|
* `ui.settings`.
|
|
2697
3099
|
*
|
|
2698
|
-
* @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.
|
|
2699
3104
|
* ```ts
|
|
2700
3105
|
* const controls = plugin.windows.create("ui/window.slint");
|
|
2701
3106
|
* controls.on("openOptions", () => plugin.settings.show("blur"));
|
|
@@ -2719,8 +3124,13 @@ export interface Settings {
|
|
|
2719
3124
|
* @capability ui.mount; `events` to follow changes.
|
|
2720
3125
|
*
|
|
2721
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.
|
|
2722
3131
|
* ```ts
|
|
2723
3132
|
* plugin.panels.backdrop.onChange((state) => {
|
|
3133
|
+
* // `null`: no backdrop is mounted.
|
|
2724
3134
|
* if (state?.effective === false) void plugin.osd.show("Video is playing");
|
|
2725
3135
|
* });
|
|
2726
3136
|
* ```
|
|
@@ -2746,7 +3156,10 @@ export interface BackdropSlot {
|
|
|
2746
3156
|
* @param listener - Receives the new state (`null`: no backdrop).
|
|
2747
3157
|
* @returns A handle that removes the listener.
|
|
2748
3158
|
*
|
|
2749
|
-
* @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.
|
|
2750
3163
|
* ```ts
|
|
2751
3164
|
* plugin.panels.backdrop.onChange((state) => console.log("backdrop", state));
|
|
2752
3165
|
* ```
|
|
@@ -2782,7 +3195,13 @@ export interface BackdropSlot {
|
|
|
2782
3195
|
*
|
|
2783
3196
|
* @capability ui.mount
|
|
2784
3197
|
*
|
|
2785
|
-
* @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
|
+
*
|
|
2786
3205
|
* {@includeCode ../../examples/panel-menu/main.ts#mount}
|
|
2787
3206
|
*
|
|
2788
3207
|
* `ui/panel.slint`:
|
|
@@ -2844,9 +3263,15 @@ export interface Panels {
|
|
|
2844
3263
|
* @throws The errors of {@link Panel.ready} (rejects `ready`) The player
|
|
2845
3264
|
* refused the mount.
|
|
2846
3265
|
*
|
|
2847
|
-
* @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.
|
|
2848
3272
|
* ```ts
|
|
2849
3273
|
* const bar = plugin.panels.mount("bottom", "ui/bar.slint");
|
|
3274
|
+
* // Sent again whenever the position changes.
|
|
2850
3275
|
* bar.bind(() => ({ position: plugin.player.state.position ?? 0 }));
|
|
2851
3276
|
* bar.on("seek", (seconds: number) => plugin.player.seek(seconds));
|
|
2852
3277
|
* await bar.ready;
|
|
@@ -2895,10 +3320,16 @@ export interface TimelineLayer extends Disposable {
|
|
|
2895
3320
|
* the player translates (`text("Chapter {}", [n])`, SDK 1.5.11); other
|
|
2896
3321
|
* plugins read the shown string.
|
|
2897
3322
|
*
|
|
2898
|
-
* @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.
|
|
2899
3328
|
* ```ts
|
|
3329
|
+
* declare const bookmarks: { time: number; note: string }[];
|
|
3330
|
+
* // ---cut---
|
|
2900
3331
|
* const layer = plugin.timeline.layer();
|
|
2901
|
-
* const seen = plugin.player.state;
|
|
3332
|
+
* const seen = plugin.player.state; // the file the bookmarks belong to
|
|
2902
3333
|
* layer.set(
|
|
2903
3334
|
* bookmarks.map((b) => ({ time: b.time, kind: "bookmark", title: b.note })),
|
|
2904
3335
|
* { ifCurrent: seen },
|
|
@@ -2955,7 +3386,13 @@ export interface WindowState {
|
|
|
2955
3386
|
* `onChange`; without it, it updates only at start and after this plugin's
|
|
2956
3387
|
* `setVideoArea`.
|
|
2957
3388
|
*
|
|
2958
|
-
* @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.
|
|
2959
3396
|
* ```ts
|
|
2960
3397
|
* plugin.commands.add({
|
|
2961
3398
|
* id: "size-1x",
|
|
@@ -2963,6 +3400,7 @@ export interface WindowState {
|
|
|
2963
3400
|
* enabled: () => !plugin.window.state.fullscreen,
|
|
2964
3401
|
* run: async () => {
|
|
2965
3402
|
* const stats = await plugin.player.stats();
|
|
3403
|
+
* // No video stream (an audio file): nothing to size to.
|
|
2966
3404
|
* if (stats.video?.width && stats.video.height)
|
|
2967
3405
|
* await plugin.window.setVideoArea({ width: stats.video.width, height: stats.video.height });
|
|
2968
3406
|
* },
|
|
@@ -3020,7 +3458,11 @@ export interface MainWindow {
|
|
|
3020
3458
|
* Where it is unsupported (Windows, Linux), `setVideoArea` plus
|
|
3021
3459
|
* `setAlwaysOnTop` make a reasonable "mini player" fallback.
|
|
3022
3460
|
*
|
|
3023
|
-
* @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.
|
|
3024
3466
|
* ```ts
|
|
3025
3467
|
* if (plugin.window.state.pictureInPicture.supported)
|
|
3026
3468
|
* await plugin.window.setPictureInPicture(!plugin.window.state.pictureInPicture.active);
|
|
@@ -3048,7 +3490,12 @@ export interface AppInfo {
|
|
|
3048
3490
|
* `plugin.app`: the fixed facts plus the system, read on request. No
|
|
3049
3491
|
* capability.
|
|
3050
3492
|
*
|
|
3051
|
-
* @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.
|
|
3052
3499
|
* ```ts
|
|
3053
3500
|
* const system = await plugin.app.system();
|
|
3054
3501
|
* const text = `Vestra ${plugin.app.version} on ${system.name} ${system.version ?? ""}`;
|
|
@@ -3147,18 +3594,28 @@ export interface ConfirmOptions {
|
|
|
3147
3594
|
* at a time: another call meanwhile rejects with `busy`. Playback continues
|
|
3148
3595
|
* while a dialog is open; command key bindings pause.
|
|
3149
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
|
+
*
|
|
3150
3601
|
* For anything more than a question or a path, use a {@link Windows | tool
|
|
3151
3602
|
* window}.
|
|
3152
3603
|
*
|
|
3153
3604
|
* @capability ui.dialog
|
|
3154
3605
|
*
|
|
3155
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.
|
|
3156
3612
|
* ```ts
|
|
3157
3613
|
* const store = plugin.storage.json("confirm", { defaults: { askBeforeDelete: true } });
|
|
3158
3614
|
* async function deleteAll(count: number) {
|
|
3159
3615
|
* if (store.value.askBeforeDelete) {
|
|
3160
3616
|
* const { confirmed, checked } = await plugin.dialogs.confirm({
|
|
3161
|
-
* message:
|
|
3617
|
+
* message: "Delete {} bookmarks?",
|
|
3618
|
+
* args: [count],
|
|
3162
3619
|
* detail: "This cannot be undone.",
|
|
3163
3620
|
* confirm: "Delete",
|
|
3164
3621
|
* destructive: true,
|
|
@@ -3205,13 +3662,22 @@ export interface Dialogs {
|
|
|
3205
3662
|
* @throws `unsupported` (rejects) A system other than macOS and Windows.
|
|
3206
3663
|
* @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
|
|
3207
3664
|
*
|
|
3208
|
-
* @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`.
|
|
3209
3670
|
* ```ts
|
|
3210
|
-
*
|
|
3211
|
-
*
|
|
3212
|
-
*
|
|
3213
|
-
*
|
|
3214
|
-
*
|
|
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
|
+
* }
|
|
3215
3681
|
* ```
|
|
3216
3682
|
*
|
|
3217
3683
|
* @since 1.0.0
|
|
@@ -3235,10 +3701,13 @@ export interface Dialogs {
|
|
|
3235
3701
|
* @throws `invalid` (rejects) `title` is longer than 256 bytes.
|
|
3236
3702
|
* @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
|
|
3237
3703
|
*
|
|
3238
|
-
* @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.
|
|
3239
3708
|
* ```ts
|
|
3240
3709
|
* const path = await plugin.dialogs.openFile({ title: "Choose a subtitle file" });
|
|
3241
|
-
* if (path !== null) await plugin.osd.show(
|
|
3710
|
+
* if (path !== null) await plugin.osd.show("Chosen: {}", [path]); // a path is data
|
|
3242
3711
|
* ```
|
|
3243
3712
|
*
|
|
3244
3713
|
* @since 1.0.0
|
|
@@ -3260,10 +3729,13 @@ export interface Dialogs {
|
|
|
3260
3729
|
* @throws `invalid` (rejects) `title` is longer than 256 bytes.
|
|
3261
3730
|
* @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
|
|
3262
3731
|
*
|
|
3263
|
-
* @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.
|
|
3264
3736
|
* ```ts
|
|
3265
3737
|
* const paths = await plugin.dialogs.openFiles({ title: "Add to the playlist" });
|
|
3266
|
-
* await plugin.osd.show(
|
|
3738
|
+
* if (paths.length) await plugin.osd.show("{} files added", [paths.length]);
|
|
3267
3739
|
* ```
|
|
3268
3740
|
*
|
|
3269
3741
|
* @since 1.0.0
|
|
@@ -3285,7 +3757,10 @@ export interface Dialogs {
|
|
|
3285
3757
|
* @throws `invalid` (rejects) `title` is longer than 256 bytes.
|
|
3286
3758
|
* @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
|
|
3287
3759
|
*
|
|
3288
|
-
* @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.
|
|
3289
3764
|
* ```ts
|
|
3290
3765
|
* const folder = await plugin.dialogs.openDirectory({ title: "Save snapshots in" });
|
|
3291
3766
|
* if (folder === null) return; // cancelled
|
|
@@ -3316,7 +3791,10 @@ export interface Dialogs {
|
|
|
3316
3791
|
* or contains `/` or `\`.
|
|
3317
3792
|
* @throws `capability` (rejects) The manifest does not declare `ui.dialog`.
|
|
3318
3793
|
*
|
|
3319
|
-
* @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.
|
|
3320
3798
|
* ```ts
|
|
3321
3799
|
* const path = await plugin.dialogs.saveFile({ title: "Save snapshot", fileName: "frame.png" });
|
|
3322
3800
|
* if (path === null) return; // cancelled
|
|
@@ -3459,10 +3937,23 @@ export interface Chapter {
|
|
|
3459
3937
|
* consecutive calls do not make each other stale. {@link Player.open} is
|
|
3460
3938
|
* unconditional unless `ifCurrent` is given.
|
|
3461
3939
|
*
|
|
3462
|
-
* @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.
|
|
3463
3945
|
* ```ts
|
|
3464
|
-
*
|
|
3465
|
-
*
|
|
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
|
+
* });
|
|
3466
3957
|
* ```
|
|
3467
3958
|
*/
|
|
3468
3959
|
export interface Conditional {
|
|
@@ -3491,10 +3982,18 @@ export interface Media {
|
|
|
3491
3982
|
* Any outside change (another plugin opens a file, the user pauses) ends
|
|
3492
3983
|
* the gesture.
|
|
3493
3984
|
*
|
|
3494
|
-
* @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`.
|
|
3495
3991
|
* ```ts
|
|
3992
|
+
* declare const bar: Panel;
|
|
3993
|
+
* // ---cut---
|
|
3496
3994
|
* let scrub: Scrub | null = null;
|
|
3497
3995
|
* bar.on("dragStart", () => (scrub = plugin.player.scrub()));
|
|
3996
|
+
* // Latest value wins: superseded targets resolve false, not an error.
|
|
3498
3997
|
* bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds));
|
|
3499
3998
|
* bar.on("dragEnd", () => scrub?.end());
|
|
3500
3999
|
* ```
|
|
@@ -3518,8 +4017,15 @@ export interface Scrub {
|
|
|
3518
4017
|
* as before. Older players ignore the option and seek exactly: set
|
|
3519
4018
|
* `"minPlayerVersion": "0.5.3"` when you rely on it.
|
|
3520
4019
|
*
|
|
3521
|
-
* @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.
|
|
3522
4025
|
* ```ts
|
|
4026
|
+
* declare const bar: Panel;
|
|
4027
|
+
* declare const scrub: Scrub | null;
|
|
4028
|
+
* // ---cut---
|
|
3523
4029
|
* const keyframes = true; // the plugin's own setting
|
|
3524
4030
|
* bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds, { keyframes }));
|
|
3525
4031
|
* ```
|
|
@@ -3715,12 +4221,17 @@ export interface PlaybackStats {
|
|
|
3715
4221
|
* `stale` and do nothing when the source changed since the state you saw.
|
|
3716
4222
|
*
|
|
3717
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.
|
|
3718
4229
|
* ```ts
|
|
3719
|
-
* const media = await plugin.player.open("/Movies/clip.mp4");
|
|
4230
|
+
* const media = await plugin.player.open("/Movies/clip.mp4"); // load accepted
|
|
3720
4231
|
* try {
|
|
3721
4232
|
* await media.ready({ signal: AbortSignal.timeout(10_000) });
|
|
3722
4233
|
* } catch (error) {
|
|
3723
|
-
* void plugin.osd.show(
|
|
4234
|
+
* void plugin.osd.show("Cannot play: {}", [(error as Error).message]);
|
|
3724
4235
|
* }
|
|
3725
4236
|
* ```
|
|
3726
4237
|
*/
|
|
@@ -3759,15 +4270,24 @@ export interface Player {
|
|
|
3759
4270
|
* (`timeout`), `stream`, `device`, or `replaced` when another source
|
|
3760
4271
|
* replaced it (or it was closed) before it ran.
|
|
3761
4272
|
*
|
|
3762
|
-
* @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.
|
|
3763
4280
|
* ```ts
|
|
3764
|
-
*
|
|
3765
|
-
*
|
|
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
|
|
3766
4285
|
* const stream = await plugin.player.openStream({
|
|
3767
4286
|
* width: 320,
|
|
3768
4287
|
* height: 240,
|
|
3769
4288
|
* frame: () => ({ video: render(frame), audio: silence }),
|
|
3770
4289
|
* });
|
|
4290
|
+
* // `error` is null when the stream stopped normally.
|
|
3771
4291
|
* stream.onStop((error) => error && plugin.osd.show(error.message));
|
|
3772
4292
|
* ```
|
|
3773
4293
|
*/
|
|
@@ -3797,8 +4317,21 @@ export interface Player {
|
|
|
3797
4317
|
* Become the sequence provider (one at a time; another provider rejects with
|
|
3798
4318
|
* `conflict` through the scope's `onError`, or fails activation). Dispose to stop.
|
|
3799
4319
|
*
|
|
3800
|
-
* @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").
|
|
3801
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.
|
|
3802
4335
|
* plugin.player.provideSequence({
|
|
3803
4336
|
* next: () => playAt(index + 1),
|
|
3804
4337
|
* previous: () => playAt(index - 1),
|
|
@@ -3932,9 +4465,22 @@ export interface Host {
|
|
|
3932
4465
|
*
|
|
3933
4466
|
* Prefer this over reimplementing playback buttons.
|
|
3934
4467
|
*
|
|
3935
|
-
* @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).
|
|
3936
4473
|
* ```ts
|
|
3937
|
-
*
|
|
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
|
+
* });
|
|
3938
4484
|
* ```
|
|
3939
4485
|
*/
|
|
3940
4486
|
run(action: HostAction): Promise<void>;
|
|
@@ -3955,8 +4501,14 @@ export interface Host {
|
|
|
3955
4501
|
* `seconds` and seek by the `setStepSeek` step)
|
|
3956
4502
|
* @since 1.5.19
|
|
3957
4503
|
*
|
|
3958
|
-
* @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.
|
|
3959
4509
|
* ```ts
|
|
4510
|
+
* declare const bar: Panel;
|
|
4511
|
+
* // ---cut---
|
|
3960
4512
|
* bar.on("forward30", () => plugin.host.run("seek-forward", { seconds: 30 }));
|
|
3961
4513
|
* ```
|
|
3962
4514
|
*/
|
|
@@ -3977,11 +4529,16 @@ export interface Host {
|
|
|
3977
4529
|
*
|
|
3978
4530
|
* @capability ui.osd
|
|
3979
4531
|
*
|
|
3980
|
-
* @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.
|
|
3981
4537
|
* ```ts
|
|
3982
4538
|
* const info = plugin.osd.panel();
|
|
3983
4539
|
* plugin.player.onChange((state) =>
|
|
3984
|
-
*
|
|
4540
|
+
* // A template with data arguments; the player translates "{} / {} s".
|
|
4541
|
+
* info.set("{} / {} s", [state.position?.toFixed(1) ?? "–", state.duration?.toFixed(1) ?? "–"]),
|
|
3985
4542
|
* );
|
|
3986
4543
|
* ```
|
|
3987
4544
|
*
|
|
@@ -4008,7 +4565,10 @@ export interface OsdPanel extends Disposable {
|
|
|
4008
4565
|
* omitted for none.
|
|
4009
4566
|
* @param options - `maxColumns`: cut long lines (SDK 1.5.11).
|
|
4010
4567
|
*
|
|
4011
|
-
* @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.
|
|
4012
4572
|
* ```ts
|
|
4013
4573
|
* const info = plugin.osd.panel();
|
|
4014
4574
|
* info.set("A: {} s\nB: {} s", ["12.0", "18.5"]);
|
|
@@ -4067,7 +4627,10 @@ export interface OsdPanelOptions {
|
|
|
4067
4627
|
*
|
|
4068
4628
|
* @capability ui.osd
|
|
4069
4629
|
*
|
|
4070
|
-
* @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.
|
|
4071
4634
|
* ```ts
|
|
4072
4635
|
* const store = plugin.storage.json("prefs", { defaults: { messages: true } });
|
|
4073
4636
|
* const say = (text: string) => {
|
|
@@ -4111,7 +4674,11 @@ export interface Osd {
|
|
|
4111
4674
|
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
4112
4675
|
* @throws `capability` (rejects) The manifest does not declare `ui.osd`.
|
|
4113
4676
|
*
|
|
4114
|
-
* @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.
|
|
4115
4682
|
* ```ts
|
|
4116
4683
|
* await plugin.osd.show("Bookmark added");
|
|
4117
4684
|
* await plugin.osd.show("Speed {}×", [plugin.player.state.rate]);
|
|
@@ -4133,7 +4700,10 @@ export interface Osd {
|
|
|
4133
4700
|
*
|
|
4134
4701
|
* @throws `Error` without a code (throws) This plugin already has a block.
|
|
4135
4702
|
*
|
|
4136
|
-
* @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.
|
|
4137
4707
|
* ```ts
|
|
4138
4708
|
* const info = plugin.osd.panel();
|
|
4139
4709
|
* info.set("Loop: off");
|
|
@@ -4211,9 +4781,14 @@ interface Filter<T> extends Disposable {
|
|
|
4211
4781
|
* Values out of range fail with `invalid` (see {@link VideoNode}). The
|
|
4212
4782
|
* same chain applies to processed snapshots and exports.
|
|
4213
4783
|
*
|
|
4214
|
-
* @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`.
|
|
4215
4789
|
* ```ts
|
|
4216
4790
|
* const picture = plugin.video.filter("picture");
|
|
4791
|
+
* // `unsupported` while a generated stream plays: its frames skip filters.
|
|
4217
4792
|
* picture.onStatus((s) => s.state === "failed" && plugin.osd.show(s.error.message));
|
|
4218
4793
|
* picture.set([{ kind: "eq", brightness: 0.1, contrast: 1.1, saturation: 1, gamma: 1 }]);
|
|
4219
4794
|
* ```
|
|
@@ -4228,10 +4803,13 @@ export type VideoFilter = Filter<VideoNode[]>;
|
|
|
4228
4803
|
* `delayMs` is −10000–10000 in multiples of 10; the positive and the
|
|
4229
4804
|
* negative delays of all plugins each add up to at most 10000.
|
|
4230
4805
|
*
|
|
4231
|
-
* @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.
|
|
4232
4810
|
* ```ts
|
|
4233
4811
|
* const audio = plugin.audio.filter("sound");
|
|
4234
|
-
* audio.set({ nodes: [{ kind: "volume", gainDb: -6 }], delayMs: 120 });
|
|
4812
|
+
* audio.set({ nodes: [{ kind: "volume", gainDb: -6 }], delayMs: 120 }); // 10 ms steps
|
|
4235
4813
|
* ```
|
|
4236
4814
|
*/
|
|
4237
4815
|
export type AudioFilter = Filter<{
|
|
@@ -4266,7 +4844,11 @@ export interface ViewSettings {
|
|
|
4266
4844
|
* Out-of-range values or unknown fields fail with `invalid` without
|
|
4267
4845
|
* sending anything.
|
|
4268
4846
|
*
|
|
4269
|
-
* @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.
|
|
4270
4852
|
* ```ts
|
|
4271
4853
|
* const view = plugin.video.view();
|
|
4272
4854
|
* view.set({ fit: "fill" });
|
|
@@ -4307,13 +4889,20 @@ export interface RenderSettings {
|
|
|
4307
4889
|
* `unsupported`. macOS OpenGL 4.1 has no compute shaders, so `peakDetect`
|
|
4308
4890
|
* is accepted but has no effect; output is always SDR.
|
|
4309
4891
|
*
|
|
4310
|
-
* @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).
|
|
4311
4898
|
* ```ts
|
|
4899
|
+
* declare function hideHdrMenu(): void;
|
|
4900
|
+
* // ---cut---
|
|
4312
4901
|
* const render = plugin.video.render();
|
|
4313
4902
|
* render.onStatus((s) => {
|
|
4314
4903
|
* if (s.state === "failed" && s.error.code === "unsupported") hideHdrMenu();
|
|
4315
4904
|
* });
|
|
4316
|
-
* render.set({ toneMapping: "bt.2390", targetPeak: 250 });
|
|
4905
|
+
* render.set({ toneMapping: "bt.2390", targetPeak: 250 }); // nits
|
|
4317
4906
|
* ```
|
|
4318
4907
|
*/
|
|
4319
4908
|
export interface RenderOptions extends Disposable {
|
|
@@ -4374,7 +4963,10 @@ export interface SubtitlesState {
|
|
|
4374
4963
|
/** Font family name for display. */
|
|
4375
4964
|
family: string;
|
|
4376
4965
|
}[];
|
|
4377
|
-
/**
|
|
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
|
+
*/
|
|
4378
4970
|
readonly style: SubtitleStyle | null;
|
|
4379
4971
|
/** Whether the renderer confirmed the selection. */
|
|
4380
4972
|
readonly trackConfirmation: "waiting" | "applied" | "uncertain";
|
|
@@ -4394,12 +4986,23 @@ export interface SubtitleConditional {
|
|
|
4394
4986
|
* `subtitles.read`, since they read the current selection first.
|
|
4395
4987
|
*
|
|
4396
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.
|
|
4397
4995
|
* ```ts
|
|
4996
|
+
* import { readFile } from "node:fs/promises";
|
|
4997
|
+
* declare const path: string;
|
|
4998
|
+
* // ---cut---
|
|
4398
4999
|
* const bytes = await readFile(path);
|
|
4399
|
-
*
|
|
5000
|
+
* await plugin.subtitles.load(bytes, { title: "English", select: "secondary" });
|
|
5001
|
+
* const { fonts } = await plugin.subtitles.refresh();
|
|
5002
|
+
* if (!fonts.length) return;
|
|
4400
5003
|
* await plugin.subtitles.setStyle({
|
|
4401
|
-
* fontId:
|
|
4402
|
-
* fontSize: 40,
|
|
5004
|
+
* fontId: fonts[0]!.id,
|
|
5005
|
+
* fontSize: 40, // at the 1080p reference height
|
|
4403
5006
|
* color: "white",
|
|
4404
5007
|
* outline: 2,
|
|
4405
5008
|
* marginPercent: 5,
|
|
@@ -4443,7 +5046,12 @@ export interface Subtitles {
|
|
|
4443
5046
|
}): Promise<SubtitleTrack>;
|
|
4444
5047
|
/** Remove a track this plugin loaded. */
|
|
4445
5048
|
remove(track: SubtitleTrack, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
4446
|
-
/**
|
|
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
|
+
*/
|
|
4447
5055
|
setStyle(style: SubtitleStyle | null, options?: SubtitleConditional): Promise<SubtitlesState>;
|
|
4448
5056
|
}
|
|
4449
5057
|
/**
|
|
@@ -4546,11 +5154,17 @@ export interface ExportJob extends Disposable {
|
|
|
4546
5154
|
* `subtitles.read`). Exports decode offline and do not disturb playback.
|
|
4547
5155
|
*
|
|
4548
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.
|
|
4549
5162
|
* ```ts
|
|
4550
5163
|
* const seen = plugin.player.state;
|
|
4551
5164
|
* const destination = await plugin.dialogs.saveFile({ fileName: "frame.jpg" });
|
|
4552
5165
|
* if (!destination) return;
|
|
4553
5166
|
* const snapshot = await plugin.media.capture({ ifCurrent: seen, processed: true });
|
|
5167
|
+
* // Release the frozen frame whatever happens to the export.
|
|
4554
5168
|
* try {
|
|
4555
5169
|
* const job = await snapshot.export({ format: "jpeg", destination, quality: 85 });
|
|
4556
5170
|
* const end = await job.done;
|
|
@@ -4610,23 +5224,28 @@ export interface WebResolverOptions {
|
|
|
4610
5224
|
* (the player shows a warning). When this plugin stops, its extensions are
|
|
4611
5225
|
* free again and such files go to the player as before.
|
|
4612
5226
|
*
|
|
4613
|
-
* @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.
|
|
4614
5235
|
* ```ts
|
|
4615
|
-
*
|
|
4616
|
-
* // The plugin's own emulator: one frame per call.
|
|
4617
|
-
*
|
|
4618
|
-
*
|
|
4619
|
-
* return () => ({ video, audio: new Float32Array(1600) });
|
|
4620
|
-
* };
|
|
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---
|
|
4621
5240
|
* plugin.resolvers.add({
|
|
4622
|
-
* id: "
|
|
4623
|
-
* extensions: ["
|
|
5241
|
+
* id: "chip8",
|
|
5242
|
+
* extensions: ["ch8"], // lower case, no dot; matches Game.CH8 too
|
|
4624
5243
|
* async resolve(path, signal) {
|
|
4625
5244
|
* const frame = emulator(await readFile(path, { signal }));
|
|
4626
5245
|
* return {
|
|
4627
5246
|
* kind: "stream",
|
|
4628
|
-
* width:
|
|
4629
|
-
* height:
|
|
5247
|
+
* width: 64, // CHIP-8's display
|
|
5248
|
+
* height: 32,
|
|
4630
5249
|
* frame,
|
|
4631
5250
|
* started: (stream) => stream.onStop((error) => error && plugin.osd.show(error.message)),
|
|
4632
5251
|
* };
|
|
@@ -4691,14 +5310,20 @@ export interface Resolver extends Disposable {
|
|
|
4691
5310
|
* files into streams this plugin generates. Needs `source.resolve` (file
|
|
4692
5311
|
* resolvers also `producer.av`).
|
|
4693
5312
|
*
|
|
4694
|
-
* @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.
|
|
4695
5318
|
* ```ts
|
|
4696
5319
|
* plugin.resolvers.add({
|
|
4697
5320
|
* id: "example",
|
|
4698
5321
|
* schemes: ["https"],
|
|
4699
5322
|
* hosts: ["video.example.com"],
|
|
4700
5323
|
* async resolve(url, signal) {
|
|
4701
|
-
* 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();
|
|
4702
5327
|
* return { kind: "media", originalUrl: url, title: page.title, primary: { url: page.stream } };
|
|
4703
5328
|
* },
|
|
4704
5329
|
* });
|
|
@@ -4721,9 +5346,13 @@ export interface Resolvers {
|
|
|
4721
5346
|
* Use it to accept such files beside the player's own media types, for
|
|
4722
5347
|
* example in a playlist's drop or folder scan.
|
|
4723
5348
|
*
|
|
4724
|
-
* @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`.
|
|
4725
5353
|
* ```ts
|
|
4726
5354
|
* const MEDIA = ["mp4", "mkv", "mp3"];
|
|
5355
|
+
* // Extensions compare in lower case, without the dot.
|
|
4727
5356
|
* const accepted = (path: string) => {
|
|
4728
5357
|
* const extension = path.split(".").pop()?.toLowerCase() ?? "";
|
|
4729
5358
|
* return MEDIA.includes(extension) || plugin.resolvers.fileTypes.includes(extension);
|
|
@@ -4742,10 +5371,13 @@ export interface Resolvers {
|
|
|
4742
5371
|
* @param listener - Receives the new list.
|
|
4743
5372
|
* @returns A handle that removes the listener.
|
|
4744
5373
|
*
|
|
4745
|
-
* @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.
|
|
4746
5378
|
* ```ts
|
|
4747
5379
|
* plugin.resolvers.onFileTypesChange((types) => {
|
|
4748
|
-
* void plugin.osd.show(
|
|
5380
|
+
* void plugin.osd.show("Also accepted: {}", [plugin.i18n.list(types)]);
|
|
4749
5381
|
* });
|
|
4750
5382
|
* ```
|
|
4751
5383
|
*
|
|
@@ -4968,10 +5600,19 @@ export interface PluginWarning {
|
|
|
4968
5600
|
* `plugin.packages`: the plugins the player knows. `list` and `onChange`
|
|
4969
5601
|
* need no capability; everything else needs `plugins.manage`.
|
|
4970
5602
|
*
|
|
4971
|
-
* @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`.
|
|
4972
5610
|
* ```ts
|
|
5611
|
+
* declare const unpackedDirectory: string;
|
|
5612
|
+
* // ---cut---
|
|
4973
5613
|
* const candidate = await plugin.packages.validate(unpackedDirectory);
|
|
4974
5614
|
* const change = await candidate.install();
|
|
5615
|
+
* // `message` says why a change did not commit.
|
|
4975
5616
|
* if ((await change.done) !== "committed") throw new Error(change.message ?? "install failed");
|
|
4976
5617
|
* ```
|
|
4977
5618
|
*/
|
|
@@ -5084,7 +5725,11 @@ export interface BindingInfo extends BindingRef {
|
|
|
5084
5725
|
* `plugin.bindings`: list and override every command's bindings, including
|
|
5085
5726
|
* the player's. Needs `bindings.manage`.
|
|
5086
5727
|
*
|
|
5087
|
-
* @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.
|
|
5088
5733
|
* ```ts
|
|
5089
5734
|
* await plugin.bindings.set({ plugin: "host", command: "mute" }, []);
|
|
5090
5735
|
* // later
|
|
@@ -5124,9 +5769,18 @@ export interface Bindings {
|
|
|
5124
5769
|
* PNG. Other systems: `unsupported`. Failures: `invalid`, `missing`,
|
|
5125
5770
|
* `unavailable`, `failed` (the system refused).
|
|
5126
5771
|
*
|
|
5127
|
-
* @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.
|
|
5128
5777
|
* ```ts
|
|
5129
|
-
*
|
|
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
|
+
* }
|
|
5130
5784
|
* ```
|
|
5131
5785
|
*/
|
|
5132
5786
|
export interface Clipboard {
|
|
@@ -5162,15 +5816,23 @@ export interface ToolResult {
|
|
|
5162
5816
|
* `plugin.tools`: ffmpeg and ffprobe, run in this plugin's process. No
|
|
5163
5817
|
* capability.
|
|
5164
5818
|
*
|
|
5165
|
-
* @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`.
|
|
5166
5825
|
* ```ts
|
|
5826
|
+
* declare const path: string;
|
|
5827
|
+
* // ---cut---
|
|
5167
5828
|
* if (await plugin.tools.available("ffprobe")) {
|
|
5168
5829
|
* const { stdout } = await plugin.tools.run(
|
|
5169
5830
|
* "ffprobe",
|
|
5170
5831
|
* ["-v", "error", "-show_format", "-of", "json", path],
|
|
5171
|
-
* { timeoutMs: 10_000 },
|
|
5832
|
+
* { timeoutMs: 10_000 }, // give up on a stuck file after 10 s
|
|
5172
5833
|
* );
|
|
5173
5834
|
* const info = JSON.parse(new TextDecoder().decode(stdout));
|
|
5835
|
+
* void info.format; // container, duration, bit rate…
|
|
5174
5836
|
* }
|
|
5175
5837
|
* ```
|
|
5176
5838
|
*/
|
|
@@ -5217,14 +5879,22 @@ export interface Storage {
|
|
|
5217
5879
|
* The file is read synchronously on creation; `debounceMs` defaults to
|
|
5218
5880
|
* 300. Files are created with mode 0600. `invalid` for a bad name.
|
|
5219
5881
|
*
|
|
5220
|
-
* @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.
|
|
5221
5888
|
* ```ts
|
|
5889
|
+
* declare const path: string;
|
|
5890
|
+
* // ---cut---
|
|
5222
5891
|
* type Settings = { resume: boolean; recent: string[] };
|
|
5223
5892
|
* const store = plugin.storage.json<Settings>("settings", {
|
|
5224
5893
|
* defaults: { resume: true, recent: [] },
|
|
5225
5894
|
* validate: (v): v is Settings =>
|
|
5226
5895
|
* typeof v === "object" && v !== null && typeof (v as Settings).resume === "boolean",
|
|
5227
5896
|
* });
|
|
5897
|
+
* // Only `save` writes: changing `store.value` in place is not saved.
|
|
5228
5898
|
* store.save({ ...store.value, recent: [path, ...store.value.recent].slice(0, 10) });
|
|
5229
5899
|
* ```
|
|
5230
5900
|
*/
|
|
@@ -5286,7 +5956,11 @@ export interface Storage {
|
|
|
5286
5956
|
* resolves, so a crash right after loses nothing; there is no debounce.
|
|
5287
5957
|
* Calls answer in order.
|
|
5288
5958
|
*
|
|
5289
|
-
* @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.
|
|
5290
5964
|
* ```ts
|
|
5291
5965
|
* // A queue the plugin is working through: after a reload or a crash it
|
|
5292
5966
|
* // goes on where it stopped, after the player quits it starts afresh.
|
|
@@ -5315,10 +5989,21 @@ export interface SessionStorage {
|
|
|
5315
5989
|
* `plugin.system`: files and links, executed by the player. No capability.
|
|
5316
5990
|
* Other systems than macOS and Windows: `unsupported`.
|
|
5317
5991
|
*
|
|
5318
|
-
* @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.
|
|
5319
5997
|
* ```ts
|
|
5320
|
-
*
|
|
5321
|
-
*
|
|
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
|
+
* }
|
|
5322
6007
|
* ```
|
|
5323
6008
|
*/
|
|
5324
6009
|
export interface System {
|
|
@@ -5346,12 +6031,25 @@ export interface System {
|
|
|
5346
6031
|
* The user denying access and other system errors reject `failed`. Never
|
|
5347
6032
|
* write secrets to settings files.
|
|
5348
6033
|
*
|
|
5349
|
-
* @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.
|
|
5350
6040
|
* ```ts
|
|
5351
|
-
*
|
|
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
|
+
* }
|
|
5352
6049
|
* if (token === null) {
|
|
5353
6050
|
* token = await askUser();
|
|
5354
|
-
*
|
|
6051
|
+
* if (token)
|
|
6052
|
+
* await plugin.secrets.set("api-token", token).catch(() => {}); // best effort
|
|
5355
6053
|
* }
|
|
5356
6054
|
* ```
|
|
5357
6055
|
*/
|