@vestra-player/sdk 1.5.20 → 1.5.23
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/dist/commands.js +2 -2
- package/dist/plugin/player.js +10 -4
- package/dist/plugin/services.js +1 -0
- package/dist/plugin/testing.d.ts +12 -0
- package/dist/plugin/testing.js +26 -1
- package/dist/plugin/types.d.ts +40 -11
- package/dist/source.d.ts +6 -0
- package/llms-full.txt +69 -22
- package/llms.txt +22 -22
- package/package.json +1 -1
- package/slint/vestra/icons.slint +3 -0
- package/slint/vestra/widgets.slint +19 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.23
|
|
4
|
+
|
|
5
|
+
**Who must change: nobody.** Republishes 1.5.22 with a clean API documentation build; 1.5.22 was tagged but never published to npm: TypeDoc treats warnings as errors and failed on two `{@link}` labels in *Playback* that contained a brace (`Scrub.seek(seconds, { keyframes })`). Documentation only; the code is the same as 1.5.22 below.
|
|
6
|
+
|
|
7
|
+
## 1.5.22
|
|
8
|
+
|
|
9
|
+
**Who must change: nobody.** Plugins built on earlier SDKs keep working unchanged: a scrub without the new option seeks exactly, as before. Additive; `apiVersion` stays `vestra/1`. The addition needs player 0.5.3.
|
|
10
|
+
|
|
11
|
+
- **`Scrub.seek(seconds, { keyframes?: boolean })`** (player 0.5.3; requested by the playback plugin for the seek bar, user ruling of 2026-10-27: the bar follows the same "Jump to keyframes only" switch as the arrow keys, PotPlayer style, no second exact seek). With `keyframes: true` the seek lands on the last keyframe at or before `seconds` (mpv `absolute+keyframes`) instead of decoding up to the exact frame: the picture appears in tens of milliseconds on any file, up to one keyframe interval before the target, and the reported position is the keyframe's. Omitted or `false`: exact, as before. The option goes on the wire only when true (`control: { kind: "seek", seconds, keyframes: true }`); players before 0.5.3 ignore the field and seek exactly, so set `"minPlayerVersion": "0.5.3"` when you rely on it. The player sends a repeated target of the same gesture only once while that seek is still running (a click is a press and a release), keyed on the target and the mode: an exact seek to the target of a keyframe seek is always sent.
|
|
12
|
+
- **`TestHost.seeks`**: every seek a plugin submitted (`player.seek`, `Scrub.seek`), newest last, as `{ seconds, keyframes }`. The test host lands on the exact target in both modes; a non-boolean `keyframes` is `invalid`.
|
|
13
|
+
- Docs: *Playback* (scrubs and the keyframe switch); *Windows, panels, settings pages* gains a pitfall: an `if` that flips under the pointer (a hover highlight created and destroyed on every hover change) rebuilds the window's accessibility tree and drops the video to 10–15 frames a second while the pointer moves; keep the element and change `visible` or `opacity`. `llms.txt` and `llms-full.txt` regenerated.
|
|
14
|
+
|
|
15
|
+
## 1.5.21
|
|
16
|
+
|
|
17
|
+
**Who must change: nobody.** Plugins built on earlier SDKs keep working unchanged; the additions need player 0.5.2. Additive and bug fixes; `apiVersion` stays `vestra/1`.
|
|
18
|
+
|
|
19
|
+
- **`Host.run("fullscreen")`** (player 0.5.2; requested by the playback plugin for a bottom-bar button): toggles the main window's fullscreen exactly like the Toggle Fullscreen command (Enter, F11): in when windowed, out when fullscreen. Read the result from `window.state.fullscreen`. `unavailable` when the player has no window; players before 0.5.2 reject the id with `invalid`, so set `"minPlayerVersion": "0.5.2"`. `HostAction` gains `"fullscreen"`.
|
|
20
|
+
- **Default mouse bindings** (player 0.5.2, user ruling): a left click on the video is Play/Pause (`Mouse:LeftClick`, fired on release) and nothing is bound to a double click; Toggle Fullscreen keeps `Enter`, `Alt+Enter`, `F11`. `BUILTIN_COMMANDS` says so. A plugin that declared `Mouse:LeftClick` itself is shadowed by the player's default, as for any key the player binds; users' own bindings are unchanged.
|
|
21
|
+
- **`ToolTipArea` no longer swallows a press** (`slint/vestra/widgets.slint`, the player's widget library; bug fix): the tip was a `PopupWindow`, so while it showed, the first press anywhere only closed it, and it took the keyboard focus. It is now Slint's input-transparent tooltip: every press reaches the control under the pointer. It shows after about half a second (was 600 ms), and `showing` follows that delay closely. No API change.
|
|
22
|
+
- **Icons `fullscreen` and `exit-fullscreen`** in `slint/vestra/icons.slint` (14 × 12, the V-Line style).
|
|
23
|
+
- **`TestHost`**: `host.run("fullscreen")` flips `host.mainWindow.fullscreen` and emits `window.state` (recorded in `coreRuns`); `host/play-pause` declares `Space` and `Mouse:LeftClick`.
|
|
24
|
+
- Docs: *Commands* (the player's mouse defaults), *Playback* (a fullscreen button), the Theme and widgets example (the tooltip); generated tables and `llms.txt` / `llms-full.txt` regenerated.
|
|
25
|
+
|
|
3
26
|
## 1.5.20
|
|
4
27
|
|
|
5
28
|
Republishes 1.5.19, which was tagged but never published: its API documentation build failed (an example title and four undocumented `IMAGE_LIMITS` fields). Documentation only; the code is the same as 1.5.19 below.
|
package/dist/commands.js
CHANGED
|
@@ -142,7 +142,7 @@ vestra.playback | reopen | Reopen Current/Last File | Ctrl+Y
|
|
|
142
142
|
vestra.playback | copy-name | Copy to Clipboard ▸ File Name
|
|
143
143
|
vestra.playback | copy-folder | Copy to Clipboard ▸ Folder Path
|
|
144
144
|
vestra.playback | copy-path | Copy to Clipboard ▸ Full Path
|
|
145
|
-
host | play-pause | Playback ▸ Play/Pause | Space
|
|
145
|
+
host | play-pause | Playback ▸ Play/Pause | Space, Mouse:LeftClick
|
|
146
146
|
host | previous | Playback ▸ Previous | PageUp, Mouse:X1
|
|
147
147
|
host | next | Playback ▸ Next | PageDown, Mouse:X2
|
|
148
148
|
host | frame-back | Playback ▸ Seek ▸ Previous Frame | D
|
|
@@ -372,7 +372,7 @@ vestra.visualizer | audio-cover | Audio ▸ Show Album Cover for Audio Only
|
|
|
372
372
|
vestra.nes | nes.open | Emulator ▸ Open NES ROM…
|
|
373
373
|
vestra.nes | nes.stop | Emulator ▸ Stop NES
|
|
374
374
|
vestra.nes | nes.window | Emulator ▸ NES Tools…
|
|
375
|
-
host | fullscreen | Toggle Fullscreen | Enter, Alt+Enter, F11
|
|
375
|
+
host | fullscreen | Toggle Fullscreen | Enter, Alt+Enter, F11
|
|
376
376
|
vestra.window | pip | Screen ▸ Picture-in-Picture
|
|
377
377
|
vestra.window | mini | Screen ▸ Mini Size | \`
|
|
378
378
|
vestra.window | size.0.5 | Screen ▸ 0.5× | 1
|
package/dist/plugin/player.js
CHANGED
|
@@ -316,7 +316,7 @@ class ScrubHandle {
|
|
|
316
316
|
!(this.#pending && sameStamp(raw.stamp, this.#pending)))
|
|
317
317
|
this.end();
|
|
318
318
|
});
|
|
319
|
-
this.#queue = new Latest(async (seconds) => {
|
|
319
|
+
this.#queue = new Latest(async ({ seconds, keyframes }) => {
|
|
320
320
|
if (!this.#active)
|
|
321
321
|
throw superseded;
|
|
322
322
|
const started = Date.now();
|
|
@@ -326,9 +326,15 @@ class ScrubHandle {
|
|
|
326
326
|
intentVersion: expected.intentVersion + 1,
|
|
327
327
|
};
|
|
328
328
|
try {
|
|
329
|
+
// `keyframes` goes on the wire only when set, so players before
|
|
330
|
+
// 0.5.3 (which ignore the field) see the same intent as before.
|
|
329
331
|
const ticket = await service.submit({
|
|
330
332
|
kind: "control",
|
|
331
|
-
control: {
|
|
333
|
+
control: {
|
|
334
|
+
kind: "seek",
|
|
335
|
+
seconds,
|
|
336
|
+
...(keyframes ? { keyframes } : {}),
|
|
337
|
+
},
|
|
332
338
|
expected,
|
|
333
339
|
});
|
|
334
340
|
if (!this.#active)
|
|
@@ -354,11 +360,11 @@ class ScrubHandle {
|
|
|
354
360
|
get active() {
|
|
355
361
|
return this.#active;
|
|
356
362
|
}
|
|
357
|
-
seek(seconds) {
|
|
363
|
+
seek(seconds, options = {}) {
|
|
358
364
|
if (!this.#active)
|
|
359
365
|
return Promise.resolve(false);
|
|
360
366
|
return this.#queue
|
|
361
|
-
.push(seconds)
|
|
367
|
+
.push({ seconds, keyframes: options.keyframes === true })
|
|
362
368
|
.catch((error) => (error === superseded ? false : Promise.reject(error)));
|
|
363
369
|
}
|
|
364
370
|
end() {
|
package/dist/plugin/services.js
CHANGED
package/dist/plugin/testing.d.ts
CHANGED
|
@@ -1147,6 +1147,18 @@ export declare class TestHost implements Transport {
|
|
|
1147
1147
|
/** The new position. */
|
|
1148
1148
|
landed: number;
|
|
1149
1149
|
}[];
|
|
1150
|
+
/**
|
|
1151
|
+
* Every seek a plugin submitted (`player.seek`, `Scrub.seek`), newest
|
|
1152
|
+
* last, with the mode it asked for. The test host lands on the exact
|
|
1153
|
+
* target either way (the player lands a `keyframes` seek on the last
|
|
1154
|
+
* keyframe at or before it); the core's step seeks are in `coreSeeks`.
|
|
1155
|
+
*/
|
|
1156
|
+
readonly seeks: {
|
|
1157
|
+
/** The target. */
|
|
1158
|
+
seconds: number;
|
|
1159
|
+
/** `Scrub.seek(seconds, { keyframes: true })` (SDK 1.5.22). */
|
|
1160
|
+
keyframes: boolean;
|
|
1161
|
+
}[];
|
|
1150
1162
|
/** Every `player.step`, newest last. */
|
|
1151
1163
|
readonly steps: {
|
|
1152
1164
|
/** `"frame"` or `"keyframe"`. */
|
package/dist/plugin/testing.js
CHANGED
|
@@ -1277,6 +1277,13 @@ export class TestHost {
|
|
|
1277
1277
|
* a keyframe, and where it landed.
|
|
1278
1278
|
*/
|
|
1279
1279
|
coreSeeks = [];
|
|
1280
|
+
/**
|
|
1281
|
+
* Every seek a plugin submitted (`player.seek`, `Scrub.seek`), newest
|
|
1282
|
+
* last, with the mode it asked for. The test host lands on the exact
|
|
1283
|
+
* target either way (the player lands a `keyframes` seek on the last
|
|
1284
|
+
* keyframe at or before it); the core's step seeks are in `coreSeeks`.
|
|
1285
|
+
*/
|
|
1286
|
+
seeks = [];
|
|
1280
1287
|
/** Every `player.step`, newest last. */
|
|
1281
1288
|
steps = [];
|
|
1282
1289
|
/** Every core action run (`host.run`, a core command, a pressed core binding), newest last. */
|
|
@@ -1375,7 +1382,7 @@ export class TestHost {
|
|
|
1375
1382
|
["Playback", "Play/Pause"],
|
|
1376
1383
|
201,
|
|
1377
1384
|
"domain",
|
|
1378
|
-
["Space"],
|
|
1385
|
+
["Space", "Mouse:LeftClick"],
|
|
1379
1386
|
false,
|
|
1380
1387
|
],
|
|
1381
1388
|
[
|
|
@@ -2735,6 +2742,7 @@ export class TestHost {
|
|
|
2735
2742
|
"volume-up",
|
|
2736
2743
|
"volume-down",
|
|
2737
2744
|
"mute",
|
|
2745
|
+
"fullscreen",
|
|
2738
2746
|
];
|
|
2739
2747
|
if (!actions.includes(String(p.action)))
|
|
2740
2748
|
throw new HostError("invalid", `unknown host action ${p.action}`);
|
|
@@ -2747,6 +2755,13 @@ export class TestHost {
|
|
|
2747
2755
|
throw new HostError("invalid", "seconds must be 0.1..3600 in tenths");
|
|
2748
2756
|
seconds = p.seconds;
|
|
2749
2757
|
}
|
|
2758
|
+
// SDK 1.5.21: the main window's Toggle Fullscreen, like Enter: in when
|
|
2759
|
+
// windowed, out when fullscreen, then `window.state`.
|
|
2760
|
+
if (p.action === "fullscreen") {
|
|
2761
|
+
this.coreRuns.push("fullscreen");
|
|
2762
|
+
this.setMainWindow({ fullscreen: !this.mainWindow.fullscreen });
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2750
2765
|
this.#runCore(String(p.action), seconds);
|
|
2751
2766
|
},
|
|
2752
2767
|
"player.setStepSeek": (p) => {
|
|
@@ -3059,6 +3074,16 @@ export class TestHost {
|
|
|
3059
3074
|
"source.state": () => this.source,
|
|
3060
3075
|
"source.submit": (p) => {
|
|
3061
3076
|
const intent = p.intent;
|
|
3077
|
+
const control = intent.control;
|
|
3078
|
+
if (intent.kind === "control" && control?.kind === "seek") {
|
|
3079
|
+
if (control.keyframes !== undefined &&
|
|
3080
|
+
typeof control.keyframes !== "boolean")
|
|
3081
|
+
throw new HostError("invalid", "keyframes must be a boolean");
|
|
3082
|
+
this.seeks.push({
|
|
3083
|
+
seconds: Number(control.seconds),
|
|
3084
|
+
keyframes: control.keyframes === true,
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3062
3087
|
const ticket = this.#intent(intent);
|
|
3063
3088
|
const operation = Number(p.operationId);
|
|
3064
3089
|
// A file this plugin's file resolver claims: pending until it answers.
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -3501,10 +3501,32 @@ export interface Media {
|
|
|
3501
3501
|
*/
|
|
3502
3502
|
export interface Scrub {
|
|
3503
3503
|
/**
|
|
3504
|
-
* Resolves `true` when this
|
|
3505
|
-
* target superseded it or the gesture ended. Rejects
|
|
3504
|
+
* Seek within the gesture. Resolves `true` when this target was applied,
|
|
3505
|
+
* `false` when a newer target superseded it or the gesture ended. Rejects
|
|
3506
|
+
* on a host failure.
|
|
3507
|
+
*
|
|
3508
|
+
* `keyframes: true` (player 0.5.3, SDK 1.5.22): land on the last keyframe
|
|
3509
|
+
* at or before `seconds` instead of decoding up to that exact frame. The
|
|
3510
|
+
* picture appears at once on any file (tens of milliseconds instead of
|
|
3511
|
+
* seconds on 4K video with keyframes far apart), up to one keyframe
|
|
3512
|
+
* interval before the target; the position the player reports is the
|
|
3513
|
+
* keyframe's. Use it when the user chose keyframe seeks (the same switch
|
|
3514
|
+
* as {@link Player.setStepSeek}), and the exact seek otherwise. The player
|
|
3515
|
+
* sends a repeated target of the same gesture only once while the seek is
|
|
3516
|
+
* still running (a click is a press and a release), but an exact seek to
|
|
3517
|
+
* the target of a keyframe seek is always sent. Omitted or `false`: exact,
|
|
3518
|
+
* as before. Older players ignore the option and seek exactly: set
|
|
3519
|
+
* `"minPlayerVersion": "0.5.3"` when you rely on it.
|
|
3520
|
+
*
|
|
3521
|
+
* @example
|
|
3522
|
+
* ```ts
|
|
3523
|
+
* const keyframes = true; // the plugin's own setting
|
|
3524
|
+
* bar.on("dragMove", (seconds: number) => void scrub?.seek(seconds, { keyframes }));
|
|
3525
|
+
* ```
|
|
3506
3526
|
*/
|
|
3507
|
-
seek(seconds: number
|
|
3527
|
+
seek(seconds: number, options?: {
|
|
3528
|
+
keyframes?: boolean;
|
|
3529
|
+
}): Promise<boolean>;
|
|
3508
3530
|
/** False once anyone else changed the source; later seeks are ignored. */
|
|
3509
3531
|
readonly active: boolean;
|
|
3510
3532
|
/** End the gesture; later seeks resolve `false`. */
|
|
@@ -3828,10 +3850,11 @@ export interface Player {
|
|
|
3828
3850
|
* step, backward on the last at or before position − step, and they
|
|
3829
3851
|
* always move. Presses during a seek add up and continue from where it
|
|
3830
3852
|
* landed; the on-screen text shows the landed time. Sources without moving
|
|
3831
|
-
* video seek exactly.
|
|
3832
|
-
*
|
|
3833
|
-
*
|
|
3834
|
-
*
|
|
3853
|
+
* video seek exactly. {@link Player.seek}, chapters, loops and
|
|
3854
|
+
* {@link Player.step} are unaffected, and so is a scrub unless it asks for
|
|
3855
|
+
* keyframes itself ({@link Scrub.seek} with `keyframes: true`, SDK 1.5.22).
|
|
3856
|
+
* Omitted or `false`: exact seeks, as before. `seconds` and `keyframes`
|
|
3857
|
+
* are written as a pair: the newest call sets both.
|
|
3835
3858
|
*
|
|
3836
3859
|
* Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
|
|
3837
3860
|
* the manifest when you rely on it.
|
|
@@ -3877,22 +3900,28 @@ export interface Player {
|
|
|
3877
3900
|
setAudioDevice(id: string | null): Promise<void>;
|
|
3878
3901
|
}
|
|
3879
3902
|
/**
|
|
3880
|
-
* The core's basic playback actions (menu commands owned by the player)
|
|
3903
|
+
* The core's basic playback actions (menu commands owned by the player),
|
|
3904
|
+
* and the main window's fullscreen toggle.
|
|
3881
3905
|
*
|
|
3882
3906
|
* | action | default bindings | behaviour |
|
|
3883
3907
|
* | --- | --- | --- |
|
|
3884
3908
|
* | `stop` | F4 | close the source |
|
|
3885
|
-
* | `play-pause` | Space | pause / resume, replay after the end |
|
|
3909
|
+
* | `play-pause` | Space, a left click on the video (player 0.5.2) | pause / resume, replay after the end |
|
|
3886
3910
|
* | `previous` / `next` | PageUp, Mouse:X1 / PageDown, Mouse:X2 | ask the sequence provider |
|
|
3887
3911
|
* | `frame-back` / `frame-forward` | D / F | `player.step("frame", …)` |
|
|
3888
3912
|
* | `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds (or `host.run`'s `seconds`), exact or to keyframes as `setStepSeek` says |
|
|
3889
3913
|
* | `rate-normal`, `rate-down`, `rate-up` | Z, X, C | 1×, ±0.1 within 0.25–4 |
|
|
3890
3914
|
* | `volume-up` / `volume-down` | ↑ / ↓, wheel | ±5 (`volume-up` stops at 100) |
|
|
3891
3915
|
* | `mute` | M | toggle mute |
|
|
3916
|
+
* | `fullscreen` | Enter, Alt+Enter, F11 | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
|
|
3892
3917
|
*
|
|
3893
|
-
* In the binding overlay their keys are `host/<action>`.
|
|
3918
|
+
* In the binding overlay their keys are `host/<action>`. `fullscreen`
|
|
3919
|
+
* changes no playback state: read the result from `window.state.fullscreen`
|
|
3920
|
+
* (`plugin.window.onChange`), which also changes when the user toggles it.
|
|
3921
|
+
* Players before 0.5.2 reject it with `invalid`: set
|
|
3922
|
+
* `"minPlayerVersion": "0.5.2"` when you use it.
|
|
3894
3923
|
*/
|
|
3895
|
-
export type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "frame-forward" | "seek-backward" | "seek-forward" | "rate-down" | "rate-up" | "rate-normal" | "volume-up" | "volume-down" | "mute";
|
|
3924
|
+
export type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "frame-forward" | "seek-backward" | "seek-forward" | "rate-down" | "rate-up" | "rate-normal" | "volume-up" | "volume-down" | "mute" | "fullscreen";
|
|
3896
3925
|
/** `plugin.host`: the player's own core actions. */
|
|
3897
3926
|
export interface Host {
|
|
3898
3927
|
/**
|
package/dist/source.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ export type SourceIntent = {
|
|
|
30
30
|
} | {
|
|
31
31
|
kind: "seek";
|
|
32
32
|
seconds: number;
|
|
33
|
+
/**
|
|
34
|
+
* Land on the last keyframe at or before `seconds` instead of the
|
|
35
|
+
* exact time (player 0.5.3). Sent only when true: older players
|
|
36
|
+
* ignore the field and seek exactly.
|
|
37
|
+
*/
|
|
38
|
+
keyframes?: boolean;
|
|
33
39
|
} | {
|
|
34
40
|
kind: "rate";
|
|
35
41
|
rate: number;
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.23: full reference
|
|
2
2
|
|
|
3
3
|
> Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
|
|
4
4
|
|
|
@@ -445,7 +445,7 @@ Key names describe keys, not characters, with two deliberate exceptions:
|
|
|
445
445
|
- **Option combinations match the physical key**, so `Alt+O` fires for ⌥O although that key types `ø`, and dead keys (⌥E, ⌥N…) fire too. On layouts that need Option to type a symbol (German ⌥5 = ``, ⌥L = `@`), a binding for the typed symbol (`"["`, `"@"`) wins over one for the physical key (`Alt+5`).
|
|
446
446
|
- **`Ctrl` is portable, `Cmd` is not.** On macOS `Ctrl+X` fires for both ⌃X and ⌘X, `Cmd+X` only for ⌘X. On Windows `Cmd` is the Windows logo key, which the system mostly keeps, so write `Ctrl+…` for a binding that works on both systems.
|
|
447
447
|
|
|
448
|
-
Mouse bindings apply only over the video area. `Mouse:LeftDoubleClick:LeftHalf` and `:RightHalf` win over the plain double click; wheel bindings count accumulated scroll steps. The right button is reserved for the context menu, and a left drag in window mode moves the window instead of clicking. A `Mouse:LeftClick` fires on release, except when the same modifiers also have a double-click binding and the click's command is not checkable: then the player waits the system's double-click interval to tell a click from a double click. (A checkable command such as play/pause runs at once; a double click runs it twice, which restores its state, and the double-click binding still fires.)
|
|
448
|
+
Mouse bindings apply only over the video area. `Mouse:LeftDoubleClick:LeftHalf` and `:RightHalf` win over the plain double click; wheel bindings count accumulated scroll steps. The right button is reserved for the context menu, and a left drag in window mode moves the window instead of clicking. A `Mouse:LeftClick` fires on release, except when the same modifiers also have a double-click binding and the click's command is not checkable: then the player waits the system's double-click interval to tell a click from a double click. (A checkable command such as play/pause runs at once; a double click runs it twice, which restores its state, and the double-click binding still fires.) The player's defaults (player 0.5.2): a left click on the video is Play/Pause and nothing is bound to a double click (up to 0.5.1 a double click was Toggle Fullscreen); a click whose command is disabled does nothing.
|
|
449
449
|
|
|
450
450
|
##### The user's bindings
|
|
451
451
|
|
|
@@ -524,7 +524,7 @@ Without this rule, a "jump to bookmark" that arrives a moment late would seek a
|
|
|
524
524
|
Two controls follow other rules because they are about the device or a gesture, not the source:
|
|
525
525
|
|
|
526
526
|
- setOutput (volume and mute) is last-write-wins and resolves `false` when a newer value replaced it.
|
|
527
|
-
- scrub() starts a drag gesture on a seek bar: each seek in the gesture is conditional on the previous one, only the newest target is kept, and any outside change ends the gesture. Use it for a slider instead of calling `seek` on every move.
|
|
527
|
+
- scrub() starts a drag gesture on a seek bar: each seek in the gesture is conditional on the previous one, only the newest target is kept, and any outside change ends the gesture. Use it for a slider instead of calling `seek` on every move. Scrub.seek with `{ keyframes: true }` (player 0.5.3, SDK 1.5.22) lands on the last keyframe at or before the target instead of the exact frame; see Step seeks: exact or keyframe.
|
|
528
528
|
|
|
529
529
|
#### Sources
|
|
530
530
|
|
|
@@ -579,16 +579,18 @@ The player's **Previous** and **Next** (menu, keys, the bottom bar) are not tied
|
|
|
579
579
|
|
|
580
580
|
#### Core actions
|
|
581
581
|
|
|
582
|
-
Play/pause, stop, previous/next, frame steps, relative seeks, speed, volume and mute are **core actions**: commands the player registers itself, so they work with no plugin running. plugin.host.run(action) runs one exactly as its menu item or key would, with the same checks and the same on-screen text. A plugin's play button should call `host.run("play-pause")` rather than `setPaused`, so that it behaves like the Space key in every case (restart after the end, the "Paused" message, disabled without a source).
|
|
582
|
+
Play/pause, stop, previous/next, frame steps, relative seeks, speed, volume and mute are **core actions**: commands the player registers itself, so they work with no plugin running. plugin.host.run(action) runs one exactly as its menu item or key would, with the same checks and the same on-screen text. A plugin's play button should call `host.run("play-pause")` rather than `setPaused`, so that it behaves like the Space key in every case (restart after the end, the "Paused" message, disabled without a source). A fullscreen button calls `host.run("fullscreen")` (player 0.5.2), which toggles the main window like Enter; show the button's state from `window.state.fullscreen`.
|
|
583
583
|
|
|
584
584
|
The difference from the `Player` methods: a `Player` control is a precise operation the plugin is responsible for (conditional, silent, its own error handling); a core action is the user's action, performed on the plugin's behalf. The action ids, menu places and default keys are in Reference: commands; how keys reach them is in Commands, menus and key bindings.
|
|
585
585
|
|
|
586
586
|
##### Step seeks: exact or keyframe
|
|
587
587
|
|
|
588
|
-
← / → (`seek-backward` / `seek-forward`) seek by the step that player.setStepSeek sets (5 s by default). By default they land on the exact time, which on a file with keyframes far apart means decoding from the previous keyframe first: a visible stall on 4K video. `setStepSeek({ seconds, keyframes: true })` (player 0.5.1, SDK 1.5.19) makes them land on a keyframe instead: forward on the first keyframe at or after position + step, backward on the last at or before position − step, and every press moves. Presses during a seek add up and continue from where it landed, and the on-screen text shows the landed time. Sources without moving video still seek exactly.
|
|
588
|
+
← / → (`seek-backward` / `seek-forward`) seek by the step that player.setStepSeek sets (5 s by default). By default they land on the exact time, which on a file with keyframes far apart means decoding from the previous keyframe first: a visible stall on 4K video. `setStepSeek({ seconds, keyframes: true })` (player 0.5.1, SDK 1.5.19) makes them land on a keyframe instead: forward on the first keyframe at or after position + step, backward on the last at or before position − step, and every press moves. Presses during a seek add up and continue from where it landed, and the on-screen text shows the landed time. Sources without moving video still seek exactly. `player.seek`, chapters, loops and `player.step` are never affected.
|
|
589
589
|
|
|
590
590
|
For more steps of your own (Ctrl+→ and the like), call `plugin.host.run("seek-forward", { seconds: 30 })`: the same checks, mode, accumulation and text as →. In keyframe mode it resolves once the seek has landed, so `player.state.position` is current afterwards; do not show your own text for it. Set `"minPlayerVersion": "0.5.1"` when you rely on either option: older players ignore them.
|
|
591
591
|
|
|
592
|
+
A seek bar follows the same switch with Scrub.seek and its `keyframes` option, `scrub.seek(seconds, { keyframes })` (player 0.5.3, SDK 1.5.22): with `keyframes: true` every click and drag step lands on the last keyframe at or before the pointer, so the picture follows the pointer on any file (an exact seek decodes from the keyframe to the target first: seconds on 4K video with keyframes far apart, and a drag freezes); the position is up to one keyframe interval early, and that is what the player shows, as PotPlayer does. Do not seek twice (keyframes, then exactly): one mode per gesture, the user's choice. Older players ignore the option and seek exactly (`"minPlayerVersion": "0.5.3"` when you rely on it).
|
|
593
|
+
|
|
592
594
|
```ts
|
|
593
595
|
const keyframes = true; // the plugin's own setting
|
|
594
596
|
await plugin.player.setStepSeek({ seconds: 5, keyframes });
|
|
@@ -596,9 +598,13 @@ const seek = (delta: number) =>
|
|
|
596
598
|
plugin.host.run(delta < 0 ? "seek-backward" : "seek-forward", {
|
|
597
599
|
seconds: Math.abs(delta),
|
|
598
600
|
});
|
|
601
|
+
// The seek bar: one gesture per press, every click and drag step in the same mode.
|
|
602
|
+
const scrub = plugin.player.scrub();
|
|
603
|
+
await scrub.seek(27.6, { keyframes });
|
|
604
|
+
scrub.end();
|
|
599
605
|
```
|
|
600
606
|
|
|
601
|
-
In tests, `TestHost.keyframes` models the source's keyframes,
|
|
607
|
+
In tests, `TestHost.keyframes` models the source's keyframes, `host.coreSeeks` records each step seek with where it landed, and `host.seeks` records each `player.seek` and `Scrub.seek` with the mode it asked for (the test host lands on the exact target in both modes).
|
|
602
608
|
|
|
603
609
|
## Concept: Video and audio processing
|
|
604
610
|
|
|
@@ -960,6 +966,7 @@ The API is the same on macOS and Windows; what differs is the system's own part:
|
|
|
960
966
|
- **Hard-coded colours.** They do not follow the palette. Use `Theme.*`, and `Theme.danger` for errors.
|
|
961
967
|
- **A `Ctrl+W` default binding.** It never fires in tool windows and floating panels. Pick another key.
|
|
962
968
|
- **Two of a kind.** One panel per plugin, one backdrop per player, one OSD info block per plugin, one open dialog at a time (`busy`).
|
|
969
|
+
- **An `if` that flips under the pointer.** `if touch.has-hover: Rectangle { … }` (a hover highlight, an accent bar, a tooltip-like box that follows the pointer) creates and destroys an element on every hover change. While a video plays, each such change costs the player's UI thread far more than a property change: the player's accessibility tree (screen readers, UI Automation) is rebuilt for the whole window each time, measured at 100–160 ms per hover on Windows, enough to drop the video to 10–15 frames a second while the pointer moves. Keep the element and change properties instead: `Rectangle { visible: touch.has-hover; … }` or `opacity:`. Use `if` for things that change rarely (a mode, a selection made with a click, a list that was loaded), never for something that follows the pointer or a hover.
|
|
963
970
|
- **Slot state without `events`.** PanelSlot.state, appearance.theme and their `onChange` listeners follow other plugins and the user only with the `events` capability.
|
|
964
971
|
|
|
965
972
|
#### Testing
|
|
@@ -2176,6 +2183,8 @@ The table lists the API whose documentation names a player version, newest last:
|
|
|
2176
2183
|
| 0.4.2 | Resolvers.fileTypes, Resolvers.onFileTypesChange |
|
|
2177
2184
|
| 0.4.3 | CommandOptions.path, PropValue, View.set, Appearance.setLanguage, TemplateArgs, Text, ConfirmOptions.args, OsdPanel.set, OsdPanelOptions.maxColumns, Osd.show |
|
|
2178
2185
|
| 0.5.1 | ImageRef, ImageOptions.maxSide, PropValue, Ui.image, Ui.remoteImage, SlotState.reason, Panel.setInteraction, PlaybackStats.presentedFps, Player.setStepSeek, Host.run, Packages.reload |
|
|
2186
|
+
| 0.5.2 | HostAction |
|
|
2187
|
+
| 0.5.3 | Scrub.seek |
|
|
2179
2188
|
|
|
2180
2189
|
The Slint library carries no version tags: `Theme.danger` needs player 0.3.0; `@vestra/icons.slint` and the palette `vestra-light` need 0.4.0 (Theme).
|
|
2181
2190
|
|
|
@@ -2410,7 +2419,7 @@ Every command the player (`host`) and its built-in plugins register when the pla
|
|
|
2410
2419
|
| Copy to Clipboard ▸ File Name | | `vestra.playback/copy-name` |
|
|
2411
2420
|
| Copy to Clipboard ▸ Folder Path | | `vestra.playback/copy-folder` |
|
|
2412
2421
|
| Copy to Clipboard ▸ Full Path | | `vestra.playback/copy-path` |
|
|
2413
|
-
| Playback ▸ Play/Pause | `Space` | `host/play-pause` |
|
|
2422
|
+
| Playback ▸ Play/Pause | `Space`, `Mouse:LeftClick` | `host/play-pause` |
|
|
2414
2423
|
| Playback ▸ Previous | `PageUp`, `Mouse:X1` | `host/previous` |
|
|
2415
2424
|
| Playback ▸ Next | `PageDown`, `Mouse:X2` | `host/next` |
|
|
2416
2425
|
| Playback ▸ Seek ▸ Previous Frame | `D` | `host/frame-back` |
|
|
@@ -2640,7 +2649,7 @@ Every command the player (`host`) and its built-in plugins register when the pla
|
|
|
2640
2649
|
| Emulator ▸ Open NES ROM… | | `vestra.nes/nes.open` |
|
|
2641
2650
|
| Emulator ▸ Stop NES | | `vestra.nes/nes.stop` |
|
|
2642
2651
|
| Emulator ▸ NES Tools… | | `vestra.nes/nes.window` |
|
|
2643
|
-
| Toggle Fullscreen | `Enter`, `Alt+Enter`, `F11
|
|
2652
|
+
| Toggle Fullscreen | `Enter`, `Alt+Enter`, `F11` | `host/fullscreen` |
|
|
2644
2653
|
| Screen ▸ Picture-in-Picture | | `vestra.window/pip` |
|
|
2645
2654
|
| Screen ▸ Mini Size | ``` | `vestra.window/mini` |
|
|
2646
2655
|
| Screen ▸ 0.5× | `1` | `vestra.window/size.0.5` |
|
|
@@ -2753,7 +2762,7 @@ The player's own playback commands, which plugin.host.run runs by id (capability
|
|
|
2753
2762
|
| Action | Menu item | Default bindings |
|
|
2754
2763
|
| --- | --- | --- |
|
|
2755
2764
|
| `stop` | Close | `F4` |
|
|
2756
|
-
| `play-pause` | Playback ▸ Play/Pause | `Space` |
|
|
2765
|
+
| `play-pause` | Playback ▸ Play/Pause | `Space`, `Mouse:LeftClick` |
|
|
2757
2766
|
| `previous` | Playback ▸ Previous | `PageUp`, `Mouse:X1` |
|
|
2758
2767
|
| `next` | Playback ▸ Next | `PageDown`, `Mouse:X2` |
|
|
2759
2768
|
| `frame-back` | Playback ▸ Seek ▸ Previous Frame | `D` |
|
|
@@ -2766,6 +2775,7 @@ The player's own playback commands, which plugin.host.run runs by id (capability
|
|
|
2766
2775
|
| `volume-up` | Audio ▸ Volume + | `ArrowUp`, `Mouse:WheelUp` |
|
|
2767
2776
|
| `volume-down` | Audio ▸ Volume − | `ArrowDown`, `Mouse:WheelDown` |
|
|
2768
2777
|
| `mute` | Audio ▸ Mute | `M` |
|
|
2778
|
+
| `fullscreen` | Toggle Fullscreen | `Enter`, `Alt+Enter`, `F11` |
|
|
2769
2779
|
|
|
2770
2780
|
The player also owns **Toggle Fullscreen** (`host/fullscreen`), **Leave Fullscreen** (`host/exit-fullscreen`, `Escape`, only in fullscreen) and **Quit** (`host/quit`); their keys are in Built-in commands.
|
|
2771
2781
|
|
|
@@ -2940,17 +2950,19 @@ Icon { name: "close"; width: 20px; height: 20px; } // scaled
|
|
|
2940
2950
|
| `add` | 10 × 10 | 1.5px |
|
|
2941
2951
|
| `remove` | 10 × 10 | 1.5px |
|
|
2942
2952
|
| `sort` | 10 × 10 | 1.5px |
|
|
2953
|
+
| `fullscreen` | 14 × 12 | 1.5px |
|
|
2954
|
+
| `exit-fullscreen` | 14 × 12 | 1.5px |
|
|
2943
2955
|
|
|
2944
2956
|
#### Widgets
|
|
2945
2957
|
|
|
2946
2958
|
`@vestra/widgets.slint`: compact controls drawn with the theme. Other controls come from `std-widgets.slint`.
|
|
2947
2959
|
|
|
2948
|
-
**`ToolTipArea`**: Hover for
|
|
2960
|
+
**`ToolTipArea`**: Hover for about half a second to show `text` beside the pointer. The tip closes when the pointer leaves, or presses on content that does not take the press itself; it shows again on the next hover. It is Slint's input-transparent tooltip: every press reaches the control under the pointer, and the tip does not take the keyboard focus. (Up to SDK 1.5.20 the tip was a PopupWindow, which swallowed the first press after it showed and took the focus while it was open.)
|
|
2949
2961
|
|
|
2950
2962
|
| Member | Kind | Default | Note |
|
|
2951
2963
|
| --- | --- | --- | --- |
|
|
2952
2964
|
| `text` | in `string` | | |
|
|
2953
|
-
| `showing` | out `bool` | | The tip is showing (read-only). |
|
|
2965
|
+
| `showing` | out `bool` | | The tip is showing (read-only); it follows the tip's delay closely, not exactly. |
|
|
2954
2966
|
|
|
2955
2967
|
**`Button`**: Dialog button: text (21px, or 18px when `small`), or an icon button when `text` is empty and `icon` holds Path data drawn in an icon-width × icon-height viewbox (filled, or 1.5px round-capped lines when `stroked`, as the @vestra/icons.slint set). The caller sizes icon buttons.
|
|
2956
2968
|
|
|
@@ -4944,8 +4956,27 @@ the gesture.
|
|
|
4944
4956
|
|
|
4945
4957
|
```ts
|
|
4946
4958
|
interface Scrub {
|
|
4947
|
-
/**
|
|
4948
|
-
|
|
4959
|
+
/**
|
|
4960
|
+
* Seek within the gesture. Resolves `true` when this target was applied,
|
|
4961
|
+
* `false` when a newer target superseded it or the gesture ended. Rejects
|
|
4962
|
+
* on a host failure.
|
|
4963
|
+
*
|
|
4964
|
+
* `keyframes: true` (player 0.5.3, SDK 1.5.22): land on the last keyframe
|
|
4965
|
+
* at or before `seconds` instead of decoding up to that exact frame. The
|
|
4966
|
+
* picture appears at once on any file (tens of milliseconds instead of
|
|
4967
|
+
* seconds on 4K video with keyframes far apart), up to one keyframe
|
|
4968
|
+
* interval before the target; the position the player reports is the
|
|
4969
|
+
* keyframe's. Use it when the user chose keyframe seeks (the same switch
|
|
4970
|
+
* as Player.setStepSeek), and the exact seek otherwise. The player
|
|
4971
|
+
* sends a repeated target of the same gesture only once while the seek is
|
|
4972
|
+
* still running (a click is a press and a release), but an exact seek to
|
|
4973
|
+
* the target of a keyframe seek is always sent. Omitted or `false`: exact,
|
|
4974
|
+
* as before. Older players ignore the option and seek exactly: set
|
|
4975
|
+
* `"minPlayerVersion": "0.5.3"` when you rely on it.
|
|
4976
|
+
*/
|
|
4977
|
+
seek(seconds: number, options?: {
|
|
4978
|
+
keyframes?: boolean;
|
|
4979
|
+
}): Promise<boolean>;
|
|
4949
4980
|
/** False once anyone else changed the source; later seeks are ignored. */
|
|
4950
4981
|
readonly active: boolean;
|
|
4951
4982
|
/** End the gesture; later seeks resolve `false`. */
|
|
@@ -5234,10 +5265,11 @@ interface Player {
|
|
|
5234
5265
|
* step, backward on the last at or before position − step, and they
|
|
5235
5266
|
* always move. Presses during a seek add up and continue from where it
|
|
5236
5267
|
* landed; the on-screen text shows the landed time. Sources without moving
|
|
5237
|
-
* video seek exactly.
|
|
5238
|
-
*
|
|
5239
|
-
*
|
|
5240
|
-
*
|
|
5268
|
+
* video seek exactly. Player.seek, chapters, loops and
|
|
5269
|
+
* Player.step are unaffected, and so is a scrub unless it asks for
|
|
5270
|
+
* keyframes itself (Scrub.seek with `keyframes: true`, SDK 1.5.22).
|
|
5271
|
+
* Omitted or `false`: exact seeks, as before. `seconds` and `keyframes`
|
|
5272
|
+
* are written as a pair: the newest call sets both.
|
|
5241
5273
|
*
|
|
5242
5274
|
* Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
|
|
5243
5275
|
* the manifest when you rely on it.
|
|
@@ -5278,23 +5310,29 @@ interface Player {
|
|
|
5278
5310
|
|
|
5279
5311
|
### HostAction
|
|
5280
5312
|
|
|
5281
|
-
The core's basic playback actions (menu commands owned by the player)
|
|
5313
|
+
The core's basic playback actions (menu commands owned by the player),
|
|
5314
|
+
and the main window's fullscreen toggle.
|
|
5282
5315
|
|
|
5283
5316
|
| action | default bindings | behaviour |
|
|
5284
5317
|
| --- | --- | --- |
|
|
5285
5318
|
| `stop` | F4 | close the source |
|
|
5286
|
-
| `play-pause` | Space | pause / resume, replay after the end |
|
|
5319
|
+
| `play-pause` | Space, a left click on the video (player 0.5.2) | pause / resume, replay after the end |
|
|
5287
5320
|
| `previous` / `next` | PageUp, Mouse:X1 / PageDown, Mouse:X2 | ask the sequence provider |
|
|
5288
5321
|
| `frame-back` / `frame-forward` | D / F | `player.step("frame", …)` |
|
|
5289
5322
|
| `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds (or `host.run`'s `seconds`), exact or to keyframes as `setStepSeek` says |
|
|
5290
5323
|
| `rate-normal`, `rate-down`, `rate-up` | Z, X, C | 1×, ±0.1 within 0.25–4 |
|
|
5291
5324
|
| `volume-up` / `volume-down` | ↑ / ↓, wheel | ±5 (`volume-up` stops at 100) |
|
|
5292
5325
|
| `mute` | M | toggle mute |
|
|
5326
|
+
| `fullscreen` | Enter, Alt+Enter, F11 | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
|
|
5293
5327
|
|
|
5294
|
-
In the binding overlay their keys are `host/<action>`.
|
|
5328
|
+
In the binding overlay their keys are `host/<action>`. `fullscreen`
|
|
5329
|
+
changes no playback state: read the result from `window.state.fullscreen`
|
|
5330
|
+
(`plugin.window.onChange`), which also changes when the user toggles it.
|
|
5331
|
+
Players before 0.5.2 reject it with `invalid`: set
|
|
5332
|
+
`"minPlayerVersion": "0.5.2"` when you use it.
|
|
5295
5333
|
|
|
5296
5334
|
```ts
|
|
5297
|
-
type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "frame-forward" | "seek-backward" | "seek-forward" | "rate-down" | "rate-up" | "rate-normal" | "volume-up" | "volume-down" | "mute";
|
|
5335
|
+
type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "frame-forward" | "seek-backward" | "seek-forward" | "rate-down" | "rate-up" | "rate-normal" | "volume-up" | "volume-down" | "mute" | "fullscreen";
|
|
5298
5336
|
```
|
|
5299
5337
|
|
|
5300
5338
|
### Host
|
|
@@ -7434,6 +7472,15 @@ class TestHost implements Transport {
|
|
|
7434
7472
|
/** The new position. */
|
|
7435
7473
|
landed: number;
|
|
7436
7474
|
}[];
|
|
7475
|
+
/**
|
|
7476
|
+
* Every seek a plugin submitted (`player.seek`, `Scrub.seek`), newest last, with the mode it asked for. The test host lands on the exact target either way (the player lands a `keyframes` seek on the last keyframe at or before it); the core's step seeks are in `coreSeeks`.
|
|
7477
|
+
*/
|
|
7478
|
+
readonly seeks: {
|
|
7479
|
+
/** The target. */
|
|
7480
|
+
seconds: number;
|
|
7481
|
+
/** `Scrub.seek(seconds, { keyframes: true })` (SDK 1.5.22). */
|
|
7482
|
+
keyframes: boolean;
|
|
7483
|
+
}[];
|
|
7437
7484
|
/** Every `player.step`, newest last. */
|
|
7438
7485
|
readonly steps: {
|
|
7439
7486
|
/** `"frame"` or `"keyframe"`. */
|
|
@@ -9730,7 +9777,7 @@ From top to bottom:
|
|
|
9730
9777
|
- **The window.** A fixed `width`; `Theme.bg-panel` instead of a hard-coded colour, so the background follows the player's palette.
|
|
9731
9778
|
- **The properties.** `level` is `in-out` because both the plugin and the user change it; `label` is only written by the plugin. `level-changed` reports a drag; `reset` reports the reset buttons.
|
|
9732
9779
|
- **The layout.** `Theme.dialog-padding` and `Theme.font-body` give the spacing and text size of the player's dialogs.
|
|
9733
|
-
- **`ToolTipArea`** shows its `text` after the pointer rests on its content for 600 ms.
|
|
9780
|
+
- **`ToolTipArea`** shows its `text` after the pointer rests on its content for about half a second (600 ms before player 0.5.2). The tip never takes a press or the keyboard focus: a click goes to the control under the pointer even while the tip shows.
|
|
9734
9781
|
- **The sliders** are bound _two-way_ to `level` with `<=>`, so dragging one moves the other at once, inside Slint. `changed` passes the value to the plugin. A one-way binding (`value: root.level`) would be broken by Slint the first time the user moved the slider. `panel: true` is the panel style; without it, the bar style.
|
|
9735
9782
|
- **`Button`** with `text` is a dialog button; `small: true` is the 18-pixel one.
|
|
9736
9783
|
- **Icon buttons** have no `text`; `icon` is SVG-style path data drawn in an `icon-width` × `icon-height` box (14×12 by default). `stroked: true` draws lines instead of filling. `checked` draws the icon in the accent colour.
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vestra Player SDK
|
|
2
2
|
|
|
3
|
-
> @vestra-player/sdk 1.5.
|
|
3
|
+
> @vestra-player/sdk 1.5.23: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process. Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n> are line numbers in llms-full.txt, next to this file: every page, declaration and example.
|
|
4
4
|
|
|
5
5
|
**Rules**
|
|
6
6
|
|
|
@@ -33,7 +33,7 @@ my-plugin/
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits:
|
|
36
|
+
Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2127–2192.
|
|
37
37
|
|
|
38
38
|
**main.ts**
|
|
39
39
|
|
|
@@ -76,9 +76,9 @@ export default async function activate(plugin: Plugin) {
|
|
|
76
76
|
- `ui.settings`: Settings.page/show
|
|
77
77
|
- `ui.theme`: Appearance.set/setLanguage
|
|
78
78
|
- `secrets`: Secrets.get/set/delete
|
|
79
|
-
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme and packages.list. Details:
|
|
79
|
+
- No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme and packages.list. Details: L2193–2235.
|
|
80
80
|
|
|
81
|
-
**Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`; `host` is a TestHost (an in-memory player): `host.setSource(…)`, `host.command(id).run()`, `host.press(key)`, `await host.settle()`, then assert on `host.osd`, `host.window(id).props`… Guide:
|
|
81
|
+
**Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`; `host` is a TestHost (an in-memory player): `host.setSource(…)`, `host.command(id).run()`, `host.press(key)`, `await host.settle()`, then assert on `host.osd`, `host.window(id).props`… Guide: L1613–1778; TestHost: L6883.
|
|
82
82
|
|
|
83
83
|
**Command line**
|
|
84
84
|
|
|
@@ -106,39 +106,39 @@ export default async function activate(plugin: Plugin) {
|
|
|
106
106
|
- `limit`: A player-wide limit was reached, a property value is over the limits of PropValue
|
|
107
107
|
- `reserved`: The id is `vestra` or starts with `vestra.`
|
|
108
108
|
- `versionMismatch`: The package needs a newer player or another `apiVersion`
|
|
109
|
-
- Others: PluginError, llms-full.txt
|
|
109
|
+
- Others: PluginError, llms-full.txt L3134.
|
|
110
110
|
|
|
111
111
|
## Reference
|
|
112
112
|
|
|
113
113
|
- [llms-full.txt](https://vestrasys.github.io/vestra-player-sdk/llms-full.txt), also node_modules/@vestra-player/sdk/llms-full.txt; the pages are on the [site](https://vestrasys.github.io/vestra-player-sdk/).
|
|
114
114
|
- Tutorials: Getting started L9–214
|
|
115
|
-
- Concepts: How a plugin runs L215–364; Commands, menus and key bindings L365–462; Playback state, controls and sources L463–
|
|
116
|
-
- How-to: Keep a slider responsive while dragging
|
|
117
|
-
- Guidelines: Interface guidelines
|
|
118
|
-
- Reference: Manifest
|
|
119
|
-
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html):
|
|
120
|
-
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html):
|
|
115
|
+
- Concepts: How a plugin runs L215–364; Commands, menus and key bindings L365–462; Playback state, controls and sources L463–608; Video and audio processing L609–686; Windows, panels, settings pages, dialogs and the OSD L687–975
|
|
116
|
+
- How-to: Keep a slider responsive while dragging L976–1150; Remember a window's state between runs L1151–1280; Translate a plugin L1281–1464; Migrate a plugin to English source text L1465–1612; Test a plugin L1613–1778; Publish a plugin L1779–1867; Migrating off package rollback (1.5.16, removed in 1.5.17) L1868–1943
|
|
117
|
+
- Guidelines: Interface guidelines L1944–2028; Menu guidelines L2029–2126
|
|
118
|
+
- Reference: Manifest L2127–2192; Capabilities L2193–2235; Limits L2236–2330; Commands, menus and bindings L2331–2799; Platforms L2800–2828; Theme L2829–3035; Command line L3036–3091
|
|
119
|
+
- [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3092–6718
|
|
120
|
+
- [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L6719–8092
|
|
121
121
|
|
|
122
122
|
## API
|
|
123
123
|
|
|
124
124
|
Where each module of `plugin` (`plugin.<module>`) is declared:
|
|
125
125
|
|
|
126
|
-
- Plugin
|
|
127
|
-
- Commands
|
|
126
|
+
- Plugin L3326, Scope L3195, PluginError L3134, Disposable L3107
|
|
127
|
+
- Commands L3538; Windows L3768; Panels L4411; Dialogs L4697; Player L5164; video: VideoFilter L5471, VideoView L5534, RenderOptions L5576; audio: AudioFilter L5495; Subtitles L5674; media: MediaCapture L5834; Resolvers L5939; Packages L6181; Osd L5407; Host L5338; Clipboard L6313; Power L6333; Tools L6366; Storage L6409; System L6432; Secrets L6450; window: MainWindow L4534; App L4615; Bindings L6283; Ui L3643; Timeline L4491; Drops L4067; Appearance L4096; I18n L4238; Settings L4344.
|
|
128
128
|
|
|
129
129
|
## Examples
|
|
130
130
|
|
|
131
131
|
Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
|
|
132
132
|
|
|
133
|
-
- hello: The README Quick start: one menu command and an on-screen message.
|
|
134
|
-
- blur: A tool window with one slider that blurs the playing video.
|
|
135
|
-
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone.
|
|
136
|
-
- bookmark: A tool window bound to the playback state: remember the position and jump back to it.
|
|
137
|
-
- panel-menu: A sidebar panel with its own right-click menu.
|
|
138
|
-
- menus: Radio menu items, separator groups and a menu item whose title changes.
|
|
139
|
-
- theme-widgets: A tool window drawn with the player's theme and widgets.
|
|
140
|
-
- markdown: A window that shows a Markdown document.
|
|
141
|
-
- i18n: A plugin in the player's language: English source text, a Chinese translation.
|
|
133
|
+
- hello: The README Quick start: one menu command and an on-screen message. L8516–8605
|
|
134
|
+
- blur: A tool window with one slider that blurs the playing video. L8606–8867
|
|
135
|
+
- signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L8868–9200
|
|
136
|
+
- bookmark: A tool window bound to the playback state: remember the position and jump back to it. L9201–9385
|
|
137
|
+
- panel-menu: A sidebar panel with its own right-click menu. L9386–9591
|
|
138
|
+
- menus: Radio menu items, separator groups and a menu item whose title changes. L9592–9751
|
|
139
|
+
- theme-widgets: A tool window drawn with the player's theme and widgets. L9752–9915
|
|
140
|
+
- markdown: A window that shows a Markdown document. L9916–10110
|
|
141
|
+
- i18n: A plugin in the player's language: English source text, a Chinese translation. L10111–10278
|
|
142
142
|
|
|
143
143
|
## Optional
|
|
144
144
|
|
package/package.json
CHANGED
package/slint/vestra/icons.slint
CHANGED
|
@@ -45,6 +45,9 @@ export global Icons {
|
|
|
45
45
|
if name == "add" { return "M 5 1.5 V 8.5 M 1.5 5 H 8.5"; }
|
|
46
46
|
if name == "remove" { return "M 1.5 5 H 8.5"; }
|
|
47
47
|
if name == "sort" { return "M 3 1.5 V 8.5 M 1.2 6.7 L 3 8.5 L 4.8 6.7 M 7 8.5 V 1.5 M 5.2 3.3 L 7 1.5 L 8.8 3.3"; }
|
|
48
|
+
// Four corners pointing out (enter) or in (leave), the default 14 × 12 box.
|
|
49
|
+
if name == "fullscreen" { return "M 1.5 4.5 V 1.5 H 4.5 M 9.5 1.5 H 12.5 V 4.5 M 12.5 7.5 V 10.5 H 9.5 M 4.5 10.5 H 1.5 V 7.5"; }
|
|
50
|
+
if name == "exit-fullscreen" { return "M 4.5 1.5 V 4.5 H 1.5 M 9.5 1.5 V 4.5 H 12.5 M 12.5 7.5 H 9.5 V 10.5 M 1.5 7.5 H 4.5 V 10.5"; }
|
|
48
51
|
return "";
|
|
49
52
|
}
|
|
50
53
|
// Line width.
|
|
@@ -5,15 +5,20 @@
|
|
|
5
5
|
import { TextEdit } from "std-widgets.slint";
|
|
6
6
|
import { Theme } from "theme.slint";
|
|
7
7
|
|
|
8
|
-
// Hover for
|
|
9
|
-
// the pointer leaves or presses
|
|
8
|
+
// Hover for about half a second to show `text` beside the pointer. The tip
|
|
9
|
+
// closes when the pointer leaves, or presses on content that does not take
|
|
10
|
+
// the press itself; it shows again on the next hover. It is Slint's
|
|
11
|
+
// input-transparent tooltip: every press reaches the control under the
|
|
12
|
+
// pointer, and the tip does not take the keyboard focus. (Up to SDK 1.5.20 the tip was a
|
|
13
|
+
// PopupWindow, which swallowed the first press after it showed and took the
|
|
14
|
+
// focus while it was open.)
|
|
10
15
|
export component ToolTipArea {
|
|
11
16
|
in property <string> text;
|
|
12
|
-
// The tip is showing (read-only)
|
|
17
|
+
// The tip is showing (read-only); it follows the tip's delay closely,
|
|
18
|
+
// not exactly.
|
|
13
19
|
out property <bool> showing;
|
|
14
|
-
//
|
|
15
|
-
// (Never assign the timer's `running`: that drops its binding
|
|
16
|
-
// tip would show only once.)
|
|
20
|
+
// Pressed during this hover: no tip until the pointer leaves.
|
|
21
|
+
// (Never assign the timer's `running`: that drops its binding.)
|
|
17
22
|
private property <bool> done;
|
|
18
23
|
area := TouchArea {
|
|
19
24
|
width: 100%; height: 100%;
|
|
@@ -22,40 +27,24 @@ export component ToolTipArea {
|
|
|
22
27
|
if !self.has-hover {
|
|
23
28
|
root.done = false;
|
|
24
29
|
root.showing = false;
|
|
25
|
-
tip.close();
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
32
|
pointer-event(event) => {
|
|
29
33
|
if event.kind == PointerEventKind.down {
|
|
30
34
|
root.done = true;
|
|
31
35
|
root.showing = false;
|
|
32
|
-
tip.close();
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
triggered => {
|
|
40
|
-
root.done = true;
|
|
41
|
-
root.showing = true;
|
|
42
|
-
tip.show();
|
|
38
|
+
// Removing it closes an open tip.
|
|
39
|
+
if root.text != "" && !root.done: Tooltip {
|
|
40
|
+
// Interpolated text is plain: `*` or `_` in it are not markup.
|
|
41
|
+
text: @markdown("\{root.text}");
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Rectangle {
|
|
50
|
-
background: Theme.bg-panel;
|
|
51
|
-
border-color: Theme.line;
|
|
52
|
-
border-width: 1px;
|
|
53
|
-
border-radius: Theme.radius-control;
|
|
54
|
-
HorizontalLayout {
|
|
55
|
-
padding-left: 5px; padding-right: 5px; padding-top: 2px; padding-bottom: 2px;
|
|
56
|
-
Text { text: root.text; color: Theme.text; font-size: Theme.font-body; }
|
|
57
|
-
}
|
|
58
|
-
}
|
|
44
|
+
Timer {
|
|
45
|
+
interval: 500ms;
|
|
46
|
+
running: area.has-hover && root.text != "" && !root.done && !root.showing;
|
|
47
|
+
triggered => { root.showing = true; }
|
|
59
48
|
}
|
|
60
49
|
}
|
|
61
50
|
|