@vestra-player/sdk 1.5.13 → 1.5.15
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 +25 -0
- package/README.md +12 -20
- package/dist/capabilities.d.ts +73 -1
- package/dist/capabilities.js +263 -28
- package/dist/commands.d.ts +89 -0
- package/dist/commands.js +438 -0
- package/dist/palettes.d.ts +23 -0
- package/dist/palettes.js +72 -0
- package/dist/player-runtime.d.ts +6 -2
- package/dist/player-runtime.js +6 -2
- package/dist/plugin/bindings.d.ts +45 -0
- package/dist/plugin/bindings.js +91 -42
- package/dist/plugin/osd.d.ts +32 -0
- package/dist/plugin/osd.js +36 -2
- package/dist/plugin/player.js +8 -0
- package/dist/plugin/services.d.ts +25 -0
- package/dist/plugin/services.js +29 -4
- package/dist/plugin/testing.d.ts +24 -1
- package/dist/plugin/testing.js +37 -7
- package/dist/plugin/types.d.ts +144 -32
- package/dist/subtitles.d.ts +5 -1
- package/dist/wire.d.ts +3 -0
- package/llms-full.txt +2683 -378
- package/llms.txt +72 -136
- package/package.json +2 -1
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vestra-player/sdk 1.5.
|
|
1
|
+
# @vestra-player/sdk 1.5.15: 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
|
|
|
@@ -10,15 +10,15 @@ Limits: 160 commands per plugin and 1024 in the player (`limit`); menu ids for t
|
|
|
10
10
|
|
|
11
11
|
This guide walks through a complete Vestra Player plugin: the files it is made of, the manifest, a first command, a window, tests, and packaging. The API reference is split into @vestra-player/sdk (the plugin API) and @vestra-player/sdk/testing (the test host); start from Plugin and Scope.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
#### How plugins run
|
|
14
14
|
|
|
15
|
-
- Each plugin runs in its own Node.js process (Node.js 26, which the player ships)
|
|
16
|
-
- The player loads the entry module and calls its **default export** `activate(plugin)`. The
|
|
17
|
-
- Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle
|
|
18
|
-
- Plugins are trusted code. Manifest capabilities are a contract that the player enforces per call (a missing one fails with `code: "capability"`), not a security sandbox.
|
|
19
|
-
- The API is the same on macOS and Windows. A feature a system cannot provide fails with `unsupported`; each method documents its platform differences.
|
|
15
|
+
- Each plugin runs in its own Node.js process (Node.js 26, which the player ships). It can use the Node standard library, the file system, the network and npm packages.
|
|
16
|
+
- The player loads the entry module and calls its **default export** `activate(plugin)`. The plugin imports the SDK **as types only**, so a built plugin contains nothing but its own code.
|
|
17
|
+
- Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. There is no cleanup function to return.
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
How a plugin runs explains the process, activation, handles, the update pass and failures in full.
|
|
20
|
+
|
|
21
|
+
#### Rules for plugins
|
|
22
22
|
|
|
23
23
|
Every plugin, in the official plugin repository or not, follows these rules. They keep plugins independent of each other and portable.
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@ Every plugin, in the official plugin repository or not, follows these rules. The
|
|
|
28
28
|
- **macOS and Windows.** The same code runs on both: build paths with `node:path`, write package paths with `/`, and handle `unsupported` where a method documents a platform difference.
|
|
29
29
|
- **Your own id.** `<owner>.<name>`, where the owner is your GitHub user or organization. `vestra` and `vestra.*` are reserved for the plugins built into the player.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
#### Install
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
34
|
mkdir my-plugin && cd my-plugin
|
|
@@ -39,7 +39,7 @@ npm i -D @vestra-player/sdk
|
|
|
39
39
|
|
|
40
40
|
The package brings its own TypeScript 5.x and `@types/node`, and the `vestra-plugin` command line tool.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
#### Files
|
|
43
43
|
|
|
44
44
|
```text
|
|
45
45
|
my-plugin/
|
|
@@ -65,7 +65,7 @@ my-plugin/
|
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
#### The manifest
|
|
69
69
|
|
|
70
70
|
```json
|
|
71
71
|
{
|
|
@@ -77,84 +77,25 @@ my-plugin/
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
| `entry` | Relative path inside the package: `.ts` during development, `.js` after building. |
|
|
86
|
-
| `capabilities` | Only names from the table below. |
|
|
87
|
-
| `minPlayerVersion` | Optional `x.y.z`: the oldest player that can load the plugin. Older players refuse it with `versionMismatch` ("requires Vestra Player ≥ x.y.z"). Set it when you use API added after SDK 1.0 (see Versions). |
|
|
80
|
+
- `id` is your plugin's identifier: `<owner>.<name>`, ASCII letters, digits, `.`, `-` and `_`.
|
|
81
|
+
- `version` is `x.y.z`; `vestra-plugin build` writes it into the package.
|
|
82
|
+
- `apiVersion` is always `"vestra/1"` for this major version of the SDK.
|
|
83
|
+
- `entry` is the entry module: `.ts` while you develop, `.js` after building.
|
|
84
|
+
- `capabilities` lists what the plugin may do. A call to a member whose capability is missing fails with `code: "capability"`.
|
|
88
85
|
|
|
89
|
-
Optional display fields (
|
|
86
|
+
Optional display fields (`name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`) are shown by the plugin store and the plugin manager, and `minPlayerVersion` names the oldest player that can load the plugin. Every field with its exact rules is in Manifest.
|
|
90
87
|
|
|
91
|
-
|
|
92
|
-
| ------------- | --------------------------------------------------------------------------------------------- |
|
|
93
|
-
| `name` | 1–64 characters, one line. |
|
|
94
|
-
| `author` | 1–64 characters, one line. |
|
|
95
|
-
| `punchline` | One-line summary, 1–120 characters. |
|
|
96
|
-
| `description` | Plain text, 1–2000 characters; line breaks allowed. |
|
|
97
|
-
| `tags` | Up to 8 tags, 1–24 characters each, unique ignoring case. |
|
|
98
|
-
| `icon` | Package path of a PNG or JPEG, at most 512 KB and 1024 pixels per side (256×256 recommended). |
|
|
99
|
-
| `readme` | Package path of a UTF-8 `.md` file (≤ 256 KB). Default: `README.md` when present. |
|
|
100
|
-
| `changelog` | Same, default `CHANGELOG.md` when present. |
|
|
101
|
-
| `sourceUrl` | `https:` address of the source code. |
|
|
102
|
-
| `license` | 1–64 characters, preferably an SPDX expression such as `MIT`. |
|
|
88
|
+
The capabilities you need first:
|
|
103
89
|
|
|
104
|
-
|
|
90
|
+
| Capability | Opens |
|
|
91
|
+
| --------------- | -------------------------------------------------------------------------------------------------- |
|
|
92
|
+
| `commands` | `plugin.commands.add` (menu items and key bindings) and `plugin.host.run` |
|
|
93
|
+
| `ui.window` | `plugin.windows.create` (tool windows) |
|
|
94
|
+
| `playback.read` | reading `plugin.player.state` (what is playing, the position), `stats`, `chapters`, timeline marks |
|
|
105
95
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
"version": "0.1.0",
|
|
110
|
-
"apiVersion": "vestra/1",
|
|
111
|
-
"entry": "main.ts",
|
|
112
|
-
"capabilities": ["commands", "ui.osd"],
|
|
113
|
-
"minPlayerVersion": "0.4.1",
|
|
114
|
-
"name": "Hello",
|
|
115
|
-
"author": "Me",
|
|
116
|
-
"punchline": "Says hello from the Tools menu.",
|
|
117
|
-
"description": "A first plugin.\nIt adds Tools ▸ Hello, which shows an on-screen message.",
|
|
118
|
-
"tags": ["example"],
|
|
119
|
-
"icon": "assets/icon.png",
|
|
120
|
-
"readme": "README.md",
|
|
121
|
-
"changelog": "CHANGELOG.md",
|
|
122
|
-
"sourceUrl": "https://github.com/me/hello",
|
|
123
|
-
"license": "MIT"
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Unknown fields, unknown capabilities and paths that leave the package directory (including through symbolic links) are rejected. The id's characters are ASCII letters, digits, `.`, `-` and `_`; the official plugin repository additionally requires `<GitHub user or organization>.<plugin name>`. The `vestra.` prefix (and the id `vestra`, in any case) is reserved for the plugins built into the player: from player 0.4.1 the player refuses to install such a package (`reserved`). Every other plugin is a third-party plugin, including those hosted in the official repository.
|
|
128
|
-
|
|
129
|
-
| Capability | Opens |
|
|
130
|
-
| -------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
131
|
-
| `commands` | `commands.add`, `host.run` |
|
|
132
|
-
| `ui.window` | `windows.create` |
|
|
133
|
-
| `ui.mount` | `panels.mount`, `panels.bottom` / `.sidebar` / `.backdrop` |
|
|
134
|
-
| `ui.dialog` | `dialogs.*` |
|
|
135
|
-
| `ui.osd` | `osd.show`, `osd.panel` |
|
|
136
|
-
| `ui.drop` | `drops.provide`, `Panel.onDrop` |
|
|
137
|
-
| `ui.theme` | `appearance.set` |
|
|
138
|
-
| `ui.settings` | `settings.page`, `settings.show` |
|
|
139
|
-
| `playback.read` | reading `player.state` at start, `refresh`, `stats`, `chapters`, `audioDevices`, `timeline.marks` |
|
|
140
|
-
| `events` | live `player.state` / `onChange`, panel visibility, subtitle and window updates |
|
|
141
|
-
| `playback.control` | `player.open`, `setPaused`, `seek`, `setRate`, … and `power.preventSleep` |
|
|
142
|
-
| `producer.av` | `player.openStream` (with `playback.control`: the stream becomes the player's source) |
|
|
143
|
-
| `processing.video` | `video.filter` (with `playback.read` and `events`), `video.view`, `video.render` (with `playback.read`) |
|
|
144
|
-
| `processing.audio` | `audio.filter` (with `playback.read` and `events`) |
|
|
145
|
-
| `subtitles.read` / `subtitles.control` | reading / changing subtitles |
|
|
146
|
-
| `media.read` / `media.control` | `media.capture`, snapshot exports |
|
|
147
|
-
| `source.resolve` | `resolvers.add` |
|
|
148
|
-
| `plugins.manage` | `packages.validate`, `rollback`, `setEnabled`, `uninstall` |
|
|
149
|
-
| `clipboard` | `clipboard.*` |
|
|
150
|
-
| `window.control` | `window.setVideoArea`, `setAlwaysOnTop`, `setCursorAutoHide`, `setPictureInPicture` |
|
|
151
|
-
| `bindings.manage` | `bindings.*` |
|
|
152
|
-
| `timeline.marks` | `timeline.layer()` |
|
|
153
|
-
| `secrets` | `secrets.*` |
|
|
154
|
-
|
|
155
|
-
No capability is needed for `tools`, `storage`, `system`, `ui.image`, `plugin.app`, reading `window.state` and `appearance.theme`, and `packages.list`.
|
|
156
|
-
|
|
157
|
-
### A first plugin
|
|
96
|
+
The plugin on this page also uses `ui.osd` (on-screen messages), `events` (follow playback changes as they happen) and `playback.control` (seeking). Every capability, with the members it opens and the capabilities they also need, is in Capabilities.
|
|
97
|
+
|
|
98
|
+
#### A first plugin
|
|
158
99
|
|
|
159
100
|
```ts
|
|
160
101
|
// main.ts
|
|
@@ -176,12 +117,12 @@ export default async function activate(plugin: Plugin) {
|
|
|
176
117
|
}
|
|
177
118
|
```
|
|
178
119
|
|
|
179
|
-
- `path` is the menu path
|
|
120
|
+
- `path` is the menu path in English; paths merge by these words in every interface language. "Tools" is a new top-level menu of this plugin. To put an item into one of the player's own menus, start the path with its id, such as `["@video", "Blur"]` (Built-in menus).
|
|
180
121
|
- `enabled` is a function: the SDK re-evaluates it after every handler and every state change it receives, and sends only changes. This needs `events` to follow outside changes.
|
|
181
122
|
- `run` may return a promise. Errors thrown or rejected there go to `plugin.onError`, then to the plugin log.
|
|
182
123
|
- Commands are registered in the background; activation succeeds once every registration made during `activate` succeeded.
|
|
183
124
|
|
|
184
|
-
|
|
125
|
+
#### A window
|
|
185
126
|
|
|
186
127
|
User interfaces are Slint files in the package. The plugin writes properties and handles callbacks; property and callback names may be written in camelCase and are converted to Slint's kebab-case.
|
|
187
128
|
|
|
@@ -223,7 +164,7 @@ This needs `ui.window`, `commands`, `playback.read`, `events` and `playback.cont
|
|
|
223
164
|
|
|
224
165
|
`@vestra/theme.slint` and `@vestra/widgets.slint` come from the player at run time. For editor support, point the Slint language server's library path `vestra` at `node_modules/@vestra-player/sdk/slint/vestra` (a read-only copy that matches the player).
|
|
225
166
|
|
|
226
|
-
|
|
167
|
+
#### Testing
|
|
227
168
|
|
|
228
169
|
`@vestra-player/sdk/testing` runs a plugin against `TestHost`, an in-memory player that speaks the real protocol, so the SDK's own logic runs unchanged. There is no decoding, Slint or native window.
|
|
229
170
|
|
|
@@ -246,11 +187,11 @@ test("shows the position", async () => {
|
|
|
246
187
|
});
|
|
247
188
|
```
|
|
248
189
|
|
|
249
|
-
Useful controls: `host.command(id).run()`, `host.press("Ctrl+Alt+W")`, `host.window(id).emit(callback, ...args)` and `.props`, `host.openElsewhere(path)`, `host.handle(method, fn)` to inject failures (throw a `HostError`), `host.settle()` to wait until the plugin's host calls are answered (it does not wait for the plugin's own file reads or child processes: poll the result you assert on, see `settle`). See TestHost.
|
|
190
|
+
Useful controls: `host.command(id).run()`, `host.press("Ctrl+Alt+W")`, `host.window(id).emit(callback, ...args)` and `.props`, `host.openElsewhere(path)`, `host.handle(method, fn)` to inject failures (throw a `HostError`), `host.settle()` to wait until the plugin's host calls are answered (it does not wait for the plugin's own file reads or child processes: poll the result you assert on, see `settle`). See TestHost and Test a plugin.
|
|
250
191
|
|
|
251
192
|
Run them with `npm test` (`vestra-plugin test`, which is `node --test tests/*.test.{ts,mjs}`).
|
|
252
193
|
|
|
253
|
-
|
|
194
|
+
#### Check, run, build and pack
|
|
254
195
|
|
|
255
196
|
```text
|
|
256
197
|
vestra-plugin check [plugin-dir...]
|
|
@@ -259,24 +200,466 @@ vestra-plugin build [plugin-dir...] [--version x.y.z]
|
|
|
259
200
|
vestra-plugin pack [plugin-dir...] [--version x.y.z] [--out dir]
|
|
260
201
|
```
|
|
261
202
|
|
|
262
|
-
- `check
|
|
263
|
-
- `build
|
|
264
|
-
- `pack
|
|
265
|
-
|
|
203
|
+
- `check` checks the manifest, then type checks the plugin's `.ts` files and `tests/` strictly.
|
|
204
|
+
- `build` checks, then writes `dist/`: the code with types stripped, the Slint and data files, and `dist/manifest.json` with the version. A runtime import of `@vestra-player/sdk` fails the build.
|
|
205
|
+
- `pack` builds, then packs `dist/` as `<id>-<version>.vxp`, the file users install and the plugin store lists.
|
|
206
|
+
|
|
207
|
+
What `build` puts into the package, how to bundle npm dependencies and the translation commands are in Command line.
|
|
266
208
|
|
|
267
209
|
During development no build is needed: start the player with `--plugin <plugin-dir>` (repeatable) to run `main.ts` directly.
|
|
268
210
|
|
|
269
|
-
|
|
211
|
+
#### Versions
|
|
212
|
+
|
|
213
|
+
Every 1.x release of the SDK implements the plugin API `vestra/1`; minor releases only add to it, and a breaking change gets a new `apiVersion` and a new major version. Some additions need a newer player: when you use one, set `minPlayerVersion` to the version it names. Player versions lists which API needs which player.
|
|
214
|
+
|
|
215
|
+
## Concept: How a plugin runs
|
|
216
|
+
|
|
217
|
+
Every other concept page builds on this one. It explains what the player does with a plugin from the moment it starts it until it stops it: the process the plugin runs in, the `activate` function, the handles everything is made of, the update pass that keeps the player in step with the plugin's state, what happens when the plugin stops, where it keeps its files, and how failures reach it.
|
|
218
|
+
|
|
219
|
+
It assumes you have written a first plugin (Getting started). For each type's members, follow the links into the API reference.
|
|
220
|
+
|
|
221
|
+
#### The plugin process
|
|
222
|
+
|
|
223
|
+
- **One Node.js process per plugin.** The player starts each plugin in a Node.js 26 process of its own (the installed player ships that Node.js). The plugin can use the Node standard library, the file system, the network and npm packages. A crash or a busy loop in one plugin does not stop the player or other plugins.
|
|
224
|
+
- **No web storage.** The player starts Node with `--no-experimental-webstorage`, so `localStorage` and `sessionStorage` do not exist. Keep settings with plugin.storage or in your own files in the data directory.
|
|
225
|
+
- **Trusted code.** Manifest capabilities are a contract the player checks on every call (a missing one fails with `code: "capability"`), not a sandbox: the process has the user's file and network access. The full list of capabilities and the API each one opens is in Reference: capabilities.
|
|
226
|
+
- **One API on every system.** The same plugin runs on macOS and Windows. Something a system cannot do fails with `unsupported`; the differences are collected in Reference: platforms.
|
|
227
|
+
- **Types only.** The plugin imports `@vestra-player/sdk` with `import type`. The `plugin` object it receives is built by the player's copy of the SDK, so a built plugin contains only its own code and keeps working with the SDK version the player ships.
|
|
228
|
+
|
|
229
|
+
The plugin and the player talk over the process's standard input and output. The SDK hides that protocol completely: a plugin only ever sees handles, promises and listeners.
|
|
230
|
+
|
|
231
|
+
#### Activation
|
|
232
|
+
|
|
233
|
+
The entry module must **default-export** `activate` (a named export is refused):
|
|
234
|
+
|
|
235
|
+
```ts nocheck
|
|
236
|
+
import type { Plugin } from "@vestra-player/sdk";
|
|
237
|
+
|
|
238
|
+
export default async function activate(plugin: Plugin) {
|
|
239
|
+
// create handles: commands, windows, panels, filters, listeners…
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
When the player starts the plugin:
|
|
244
|
+
|
|
245
|
+
1. It starts the process, and the SDK loads the entry module.
|
|
246
|
+
2. The SDK reads the initial state the plugin may see (the playback state with `playback.read`, the main window and timeline state) and, with `events`, subscribes to changes.
|
|
247
|
+
3. It calls `activate(plugin)`.
|
|
248
|
+
4. It waits until every registration started during activation has succeeded: commands, panels, settings pages, resolvers, subscriptions. Then it reports the plugin ready.
|
|
249
|
+
|
|
250
|
+
You do not need to `await` those registrations. `commands.add`, `panels.mount` and `resolvers.add` return their handle at once and register in the background; their `ready` promise is there for the rare case that you want to wait. **Activation fails** when `activate` throws or rejects, or when one of those registrations fails. A registration made later, after activation, cannot fail activation any more: its failure goes to `onError`.
|
|
251
|
+
|
|
252
|
+
Some refusals are deliberately not failures, because they depend on which other plugins are installed: a bottom bar or backdrop that another plugin's panel already holds, the next/previous sequence that another plugin provides, a file type or web host that another plugin already resolves. The plugin keeps running without that part, the player tells the user, and the plugin manager shows it as a warning (PluginInfo.warnings). The player's built-in plugins register first, so they win these contests at start-up.
|
|
253
|
+
|
|
254
|
+
#### Handles and scopes
|
|
255
|
+
|
|
256
|
+
Everything a plugin creates is a **handle**: an object that stands for one resource, carries its operations and its listeners, and implements Disposable. A command, a window, a panel, a filter chain, a snapshot, an export job, a resolver, a generated stream and every listener (`onChange`, `bind`, `on`…) are handles.
|
|
257
|
+
|
|
258
|
+
```text
|
|
259
|
+
plugin (root scope)
|
|
260
|
+
├─ command "open-inspector"
|
|
261
|
+
├─ window "inspector" ← view.on / view.bind listeners belong to it
|
|
262
|
+
├─ video filter "picture"
|
|
263
|
+
└─ child scope "tracks" ← plugin.scope()
|
|
264
|
+
├─ command "track.0"
|
|
265
|
+
└─ command "track.1"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
- Every handle belongs to the Scope it was created through. Plugin is the root scope; plugin.scope() makes a child with exactly the same modules.
|
|
269
|
+
- Disposing a scope disposes its handles and deeper scopes in reverse creation order. Disposing twice is harmless, and `dispose()` never rejects (a failure is only logged).
|
|
270
|
+
- **Child scopes are for things that come and go together.** A menu that lists the current tracks, a window and the timer that feeds it, a feature the user can switch off: create them in a child scope and dispose the scope to remove them all, then build a new one. Titles and menu paths cannot change in place, so this is also how a changing menu is rebuilt (see the example on Scope).
|
|
271
|
+
- add(handle) hands an existing handle to another scope; onDispose(cleanup) runs your own cleanup (a `clearInterval`, closing a socket) with the scope.
|
|
272
|
+
- What disposing does depends on the handle (a window closes, a filter chain is withdrawn, a stream stops…); Disposable lists it. The one exception is Media: disposing its scope does not close what the plugin opened, because the user is watching it. Call `media.close()` when you mean it.
|
|
273
|
+
|
|
274
|
+
There is no cleanup function to return from `activate`. Because every resource is a handle owned by a scope, stopping the plugin releases everything.
|
|
275
|
+
|
|
276
|
+
#### The update pass
|
|
277
|
+
|
|
278
|
+
A plugin's state is plain variables. The SDK keeps the player in step with it through **bindings** and the **update pass**:
|
|
279
|
+
|
|
280
|
+
- A binding is a function the SDK calls to get a current value: view.bind(() => props) for a window, panel or settings page, and the function form of a command's `enabled`, `checked`, `busy` and `reason` (Bindable).
|
|
281
|
+
- In an update pass the SDK calls every binding of the plugin, compares each result with the value it last sent, and sends only what changed.
|
|
282
|
+
|
|
283
|
+
Update passes run:
|
|
284
|
+
|
|
285
|
+
- after every handler the SDK calls returns (a command's `run`, a view callback, an `onChange`, `onStatus`, `onEnd` or `onStop` listener), and again when the promise it returned settles;
|
|
286
|
+
- after the player, subtitle, panel position, filter or export state changes;
|
|
287
|
+
- when the plugin calls plugin.update().
|
|
288
|
+
|
|
289
|
+
Several requests within one microtask are merged into one pass, and every target has at most one write in flight: values that change meanwhile are merged into the next write. A binding that changes 60 times a second therefore costs one write per round trip, never a growing queue.
|
|
290
|
+
|
|
291
|
+
One click, end to end (a bookmark window; the video is at 42 s):
|
|
292
|
+
|
|
293
|
+
```text
|
|
294
|
+
user clicks "Mark this position"
|
|
295
|
+
─▶ Slint: clicked => { root.mark(); }
|
|
296
|
+
─▶ the player forwards the callback to the plugin
|
|
297
|
+
─▶ window.on("mark") handler runs: mark = 42
|
|
298
|
+
handler returns ─▶ update pass: every binding is evaluated
|
|
299
|
+
label "Bookmark: 42.0 s" changed → sent
|
|
300
|
+
canMark true same → not sent
|
|
301
|
+
canJump true changed → sent
|
|
302
|
+
─▶ the label changes; "Jump to bookmark" is enabled
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The rule that follows: **state you change outside a handler, or after an `await` inside one, appears only at the next pass.** Changing a variable from your own `setInterval`, a socket callback or the middle of a long task shows nothing until something triggers a pass; call `plugin.update()` after such a change. Keep a slider responsive while dragging shows the pattern; How Slint and TypeScript talk explains `set` versus `bind` for views.
|
|
306
|
+
|
|
307
|
+
#### Stopping
|
|
308
|
+
|
|
309
|
+
The player stops a plugin when the user disables or uninstalls it, when it is reloaded or upgraded, and when the player quits.
|
|
310
|
+
|
|
311
|
+
1. The player first removes everything the plugin owns on its side: menu items, windows, panels, settings pages, filter chains, subtitle layers, snapshots, export jobs, resolvers, generated streams.
|
|
312
|
+
2. The SDK then refuses new requests, fails pending ones with `code: "disabled"` and disposes the root scope. That disposal only cleans up locally (timers, abort signals, sockets): the player-side resources are already gone.
|
|
313
|
+
3. A process that has not exited 300 ms later is killed.
|
|
314
|
+
|
|
315
|
+
So `onDispose` cleanup must be quick and must not rely on the player. Media the plugin opened with player.open **keeps playing** after it stops.
|
|
316
|
+
|
|
317
|
+
#### The data directory
|
|
318
|
+
|
|
319
|
+
plugin.dataDirectory is a writable directory for the plugin's own files (settings, caches, remembered folders). It needs no capability; the player creates it before it starts the plugin.
|
|
320
|
+
|
|
321
|
+
- **One directory per installation.** Each fresh installation of a plugin gets its own directory (player 0.2.0). Installing the same id again from the same source is an upgrade and keeps the directory. Installing the same id from a different source, and confirming the replacement, gives the new plugin a new, empty directory; the old one is kept and reconnected if the user goes back to the first source later. A development load (`--plugin`) has a directory of its own.
|
|
322
|
+
- **Kept** when the plugin is disabled, reloaded, upgraded or rolled back. Uninstalling deletes it only when the user asks for that (uninstall with `removeData: true`); otherwise reinstalling from the same source reconnects it.
|
|
323
|
+
- **The path's layout is not part of the API.** Do not derive another plugin's directory from yours; a plugin manager reads it from PluginInfo.dataDirectory.
|
|
324
|
+
- **Write atomically.** Write to a temporary name and rename it, so a player that quits mid-write never leaves half a file; fall back to defaults when a file is missing or unreadable. For JSON settings use plugin.storage.json, which already does this (defaults, debounced atomic writes, a corrupt file moved aside).
|
|
325
|
+
- Reading the property throws `unavailable` when the player could not provide a directory.
|
|
326
|
+
|
|
327
|
+
Credentials do not belong in files: keep them in the system's secret store with plugin.secrets.
|
|
328
|
+
|
|
329
|
+
In tests, `TestHost` gives each test an empty temporary directory and deletes it on `stop()`; pass the same `dataDirectory` to two runs to test what survives a restart (Test a plugin).
|
|
330
|
+
|
|
331
|
+
#### Failures and recovery
|
|
332
|
+
|
|
333
|
+
Every failure is an ordinary `Error` with a stable `code` (PluginError, which has the table of codes). **Branch on `code`, never on `message`**: the message is for logs and people, and it may change or be translated.
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
try {
|
|
337
|
+
await plugin.player.seek(30);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if ((error as PluginError).code === "stale") return; // the user changed the source meanwhile
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Where a failure goes:
|
|
345
|
+
|
|
346
|
+
- **A promise you await** rejects, and you handle it like any other error.
|
|
347
|
+
- **A handler the SDK calls** (a command's `run`, a view callback, a listener) that throws or rejects reports to onError of its scope; without a listener, to the parent scope, and finally to the plugin log. So you do not need a `try`/`catch` in every handler; one `onError` that shows the problem is often enough:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
plugin.onError((error) => {
|
|
351
|
+
void plugin.osd.show(`Bookmarks: ${error.message}`);
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- **A registration after activation** (a command whose `ready` rejects, a property write the player refused) also reports to `onError`.
|
|
356
|
+
- **A promise you forgot to `await`** that rejects is only logged. It never crashes the plugin process.
|
|
357
|
+
- **A lasting target** reports through its status instead: a filter chain, the display layer and the render options are `waiting`, `applying`, `applied` or `failed` (FilterStatus); see Video and audio processing.
|
|
358
|
+
|
|
359
|
+
Three properties make failures predictable:
|
|
360
|
+
|
|
361
|
+
- **A refusal did nothing.** A call that fails with a player refusal (`stale`, `busy`, `conflict`, `invalid`, `unavailable`, `unsupported`) was not carried out, so it is safe to show the reason and let the user try again. `stale` in particular means the state your call depended on changed; see conditional controls.
|
|
362
|
+
- **No client-side timeouts.** The player answers every request in order, or stops the plugin. A slow request is logged after 10 seconds, but the SDK keeps waiting for the real answer instead of guessing, so an operation never ends in "maybe".
|
|
363
|
+
- **No blind retries.** The SDK never resends an operation that is not safe to repeat (opening, loading subtitles, starting an export, installing). Only last-write-wins targets such as filter chains are sent again, and only when the SDK knows the earlier write did not take effect. A filter chain that failed with `backend-uncertain` (the player's backend blocked that stage) waits for retry(), the next `set()` or a new source.
|
|
364
|
+
|
|
365
|
+
## Concept: Commands, menus and key bindings
|
|
366
|
+
|
|
367
|
+
A plugin offers actions to the user through **commands**. One command can appear as a menu item, run from a key or mouse binding, sit in a panel's context menu and be run by tests, and its state (enabled, checked, busy) is the same everywhere. This page explains that model, how menu paths from all plugins merge into the player's one menu, and how the player decides which command a key press runs.
|
|
368
|
+
|
|
369
|
+
Where a command should go and how to title it are rules, not concepts: see Guidelines: menus. The names to look up (built-in menu ids, shared menu keys, binding syntax and input names, the player's own commands, keys the system takes first) are in Reference: commands.
|
|
370
|
+
|
|
371
|
+
#### Commands and state
|
|
372
|
+
|
|
373
|
+
plugin.commands.add declares a command (capability `commands`) and returns a Command handle:
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
const toggle = plugin.commands.add({
|
|
377
|
+
id: "toggle-pause",
|
|
378
|
+
path: ["@playback", "Pause"],
|
|
379
|
+
bindings: ["Ctrl+P"],
|
|
380
|
+
enabled: () => plugin.player.state.can.pause,
|
|
381
|
+
checked: () => plugin.player.state.paused,
|
|
382
|
+
run: () => plugin.player.setPaused(!plugin.player.state.paused),
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
- **The id is the command's identity.** It is unique within the plugin; the player knows the command as `<manifest id>/<id>`. The user's own key bindings are stored under that pair, so changing a title or moving the command to another menu keeps them.
|
|
387
|
+
- **`path` puts it in the main menu.** Leave it out for a command that only has bindings or lives in a panel's context menu (Panel.showMenu); it then needs a `title`.
|
|
388
|
+
- **State is declared, not pushed.** `enabled`, `checked`, `busy` and `reason` are values or functions (Bindable). Functions are evaluated in every update pass and sent only when they change, so the menu's check mark follows the player without a listener. Command.set replaces them later.
|
|
389
|
+
- **The player calls `run` only when it can run.** While a command is disabled or busy, neither its menu item nor its binding calls `run`; `reason` tells the user why (it appears in the menu and, for a key press, on the video).
|
|
390
|
+
- **Titles and paths are fixed.** They cannot change in place. A menu that follows changing content (tracks, presets, a radio group) lives in a child scope that is disposed and rebuilt when the content changes; `order` keeps its place (Handles and scopes).
|
|
391
|
+
- Disposing the command removes it from the menu and the binding table; the same id can be added again at once. The number of commands per plugin and per player is limited (Reference: limits); the limit stops runaway menus, so cap menus built from media content (tracks, chapters) yourself.
|
|
392
|
+
|
|
393
|
+
#### How menu paths merge
|
|
394
|
+
|
|
395
|
+
The player has **one main menu**, built from the paths of every command of every plugin and of the player itself.
|
|
396
|
+
|
|
397
|
+
- **Paths are English source text, and they are keys.** `["@video", "Picture", "Brightness…"]` names a place in the menu. Commands of different plugins whose leading segments are the same share that submenu: two plugins that both write `["@playback", "A-B Loop", …]` put their items into one **A-B Loop** submenu.
|
|
398
|
+
- **Each segment is shown translated by the plugin that declared it.** The player looks every segment and the title up in the declaring plugin's catalog (Translate a plugin). A submenu shared by several plugins takes its label from the first command in it. The player's own menus (the seven top-level menus and **Playback ▸ Speed**, **Playback ▸ Seek**) always show the player's words.
|
|
399
|
+
- **Built-in menus** are named by id or by key: `@video` and `Video` are the same menu. The id form is safer, because it fails with `invalid` when misspelled, and players up to 0.4.2 titled their menus in Chinese, where only `@video` reaches the built-in menu. Only the first segment is read as an id.
|
|
400
|
+
- **Shared submenus need a shared key.** Plugins that should meet in one submenu must spell its key identically. The keys already in use (by the player and its built-in plugins) are listed in Reference: commands; reuse one instead of inventing a near-synonym.
|
|
401
|
+
- **A segment with data** (a track number, a count) is a Text from `plugin.i18n.text(…)`: it is shown translated and filled in, and it merges by its English rendering ("Track 2 (opus)").
|
|
402
|
+
|
|
403
|
+
The same path cannot be registered twice, and a path cannot be both an item and a submenu: the second registration fails with `conflict`.
|
|
404
|
+
|
|
405
|
+
##### Order and separators
|
|
406
|
+
|
|
407
|
+
- Siblings sort by `order`, ascending. A submenu sits where the first command inside it sits, and takes that command's `group`. Equal orders sort by plugin id, then declaration order, **never by title** (a translated title would reorder the menu per language).
|
|
408
|
+
- `group` only draws separators: a line appears between neighbours of different groups. To keep a group together, keep its orders contiguous.
|
|
409
|
+
- **Built-in menus keep their place.** The seven built-in top-level menus sit at fixed orders, whatever plugins put inside them. Directly inside a built-in menu, `order` 0 (the default) sorts after every other item, so a plugin item without `order` goes to the end, after the player's own items; a positive order places it between them, a negative one first. Deeper levels have no such rule.
|
|
410
|
+
|
|
411
|
+
The orders of the player's own items and the recommended top-level ranges are on CommandOptions.
|
|
412
|
+
|
|
413
|
+
#### How a key finds its command
|
|
414
|
+
|
|
415
|
+
A binding is a string such as `"Ctrl+Shift+A"`, `"NumpadEnter"` or `"Mouse:WheelUp"` (Reference: commands has the syntax and every input name). When the user presses a key, the player looks for a command in this order:
|
|
416
|
+
|
|
417
|
+
```text
|
|
418
|
+
key press in the main window or the floating sidebar
|
|
419
|
+
│ a focused control that uses the key (Space in a text field) keeps it
|
|
420
|
+
▼
|
|
421
|
+
this plugin's panel has focus? ── yes ─▶ its focus: "panel" bindings first
|
|
422
|
+
▼
|
|
423
|
+
global bindings
|
|
424
|
+
│ several commands bind the same input in the same scope:
|
|
425
|
+
│ 1. the user's override 2. the player's own command 3. the plugin loaded first
|
|
426
|
+
▼
|
|
427
|
+
winner enabled ──────────────▶ run() (busy: the key is taken, nothing runs)
|
|
428
|
+
winner disabled ─────────────▶ the next command it shadowed, then the global scope;
|
|
429
|
+
none left: its reason on the OSD (key taken),
|
|
430
|
+
no reason: the key goes on to the focused control
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
- **Where bindings apply.** Bindings belong to the main window: they fire while it or the floating sidebar window has focus. They do **not** fire in tool windows or the Options window; keys there are the Slint file's own business (a `FocusScope`, a default button). While a menu or a system dialog is open, bindings pause.
|
|
434
|
+
- **Conflicts are not errors.** A binding that loses is written to the player's log and takes effect by itself when the winner goes away (its plugin stops, the user rebinds it). Tests can check `host.conflicts` (TestHost.conflicts).
|
|
435
|
+
- **A disabled command lets its key go.** This is what makes context-dependent keys work: `Escape` bound to "Leave Fullscreen" does nothing outside fullscreen, so `Escape` reaches the focused control. Give `reason` to a command whose key should still be taken and explained while it is disabled.
|
|
436
|
+
- **`repeat: true`** fires again on the keyboard's auto-repeat while the key is held; without it a binding fires once per press.
|
|
437
|
+
- **Some keys never reach a binding.** The system takes some keys first, and the standard close keys close a tool window, the floating sidebar or the Options window before any binding sees them (in the main window they are ordinary keys). Keys the system takes first lists them; default bindings avoid them.
|
|
438
|
+
|
|
439
|
+
##### What a key name matches
|
|
440
|
+
|
|
441
|
+
Key names describe keys, not characters, with two deliberate exceptions:
|
|
442
|
+
|
|
443
|
+
- **Letters and named keys match the physical key.** `Shift+E` is Shift and E, independent of Caps Lock; `Delete` is the forward-delete key and `Backspace` the ⌫ key; ⌥⌦ is `Alt+Delete`. The numeric keypad has names of its own (`"5"` is only the main 5, `"Numpad5"` only the keypad's).
|
|
444
|
+
- **Symbols match the typed character.** Write `">"`, not `"Shift+."`, and `"!"`, not `"Shift+1"`; `+` is always `Plus`, however the layout produces it.
|
|
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
|
+
- **`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
|
+
|
|
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.)
|
|
449
|
+
|
|
450
|
+
##### The user's bindings
|
|
451
|
+
|
|
452
|
+
The user can rebind any command, the player's own included. Those **overrides** win over every default binding, are kept across launches, and are stored by plugin id and command id, never by menu path. A plugin with the capability `bindings.manage` (a key-binding settings page) reads and changes them with plugin.bindings:
|
|
453
|
+
|
|
454
|
+
- `list()` returns every declared command with its default and current inputs, which of them another command shadows, and the overrides whose command does not exist right now (its plugin is stopped); those are kept and apply again when it returns.
|
|
455
|
+
- To find a command by its place in the menu, compare BindingInfo.key (the English menu keys, the same in every language), not `path`, which is the translated text.
|
|
456
|
+
|
|
457
|
+
#### The player's own commands
|
|
458
|
+
|
|
459
|
+
The player registers some commands itself, with the owner `host`: **Toggle Fullscreen**, **Leave Fullscreen**, **Quit** and the core playback actions (play/pause, stop, previous/next, frame step, relative seek, speed, volume, mute). They exist whether or not any plugin runs, and the user rebinds them like any other command.
|
|
460
|
+
|
|
461
|
+
A plugin button that does what a core action does should run that action with plugin.host.run (capability `commands`): it applies the same checks and shows the same on-screen text as the menu item and the key, so a bottom bar's play button and the Space key behave identically. The action ids, their menu places and default keys are in [Reference: commands; what they do to playback is in Playback.
|
|
462
|
+
|
|
463
|
+
## Concept: Playback state, controls and sources
|
|
464
|
+
|
|
465
|
+
The player plays one source at a time, and many parties change it: the user, the player's own keys and every plugin. This page explains how a plugin sees that shared state, why its controls are conditional on what it saw, where sources come from (files and URLs, resolvers, streams a plugin generates), and how next/previous and the core actions fit in.
|
|
466
|
+
|
|
467
|
+
The members are on Player; this page explains how they work together.
|
|
468
|
+
|
|
469
|
+
#### The playback state
|
|
470
|
+
|
|
471
|
+
plugin.player.state is a PlayerState: an immutable snapshot of the current source.
|
|
472
|
+
|
|
473
|
+
```text
|
|
474
|
+
open() media ready played to the end
|
|
475
|
+
empty ───────────▶ loading ─────────────────────▶ ready ─────────────────────▶ ended
|
|
476
|
+
▲ │ │ ▲ │
|
|
477
|
+
│ └── load failed ──▶ error │ └──── restart() ───────────┘
|
|
478
|
+
└──────────── close() / F4 ───────────────────────┘
|
|
479
|
+
every open (even of the same file) starts a new sourceId
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
- **`sourceId` identifies a source.** It changes every time something is opened, even the same file again, so comparing `sourceId`s tells you whether "the thing playing" is still the one you saw.
|
|
483
|
+
- **`can` says what the source allows**: pause, seek, rate, restart, close, snapshot, next, previous. A live stream cannot seek; a generated stream cannot seek, change speed or restart. Bind your controls' enabled state to it instead of guessing from `kind` or the file type.
|
|
484
|
+
- **`kind` is `"media"` or `"stream"`**, a source the player decodes or one a plugin generates. Tell them apart by `kind`, never by parsing `source`.
|
|
485
|
+
|
|
486
|
+
Reading it needs `playback.read`; following it needs `events` too:
|
|
487
|
+
|
|
488
|
+
- The SDK reads the state once before `activate` and, with `events`, keeps `player.state` current and calls onChange listeners after every change. The state **never goes backwards**: a late event that is older than what the plugin already has is dropped.
|
|
489
|
+
- Without `events`, `player.state` stays as it was first read until you call refresh().
|
|
490
|
+
- Some facts are read on demand instead of pushed, because they are large or rarely needed: chapters(), stats() (codecs, frame counts, decoder, bitrates), audioDevices().
|
|
491
|
+
- In `stats()`, the frame counts come from two places. `framesShown` and `framesDropped` are the video renderer's (mpv's) counts. `presentedFps` is what the player window really showed: new frames per second over the last second. When it stays well below `video.fps` while playing, frames are lost after rendering (the window cannot keep up, or the display refreshes slower). `video.decoding` says whether the video is decoded in `"hardware"` (the method in `video.hwdec`) or `"software"`.
|
|
492
|
+
|
|
493
|
+
Because the state is a snapshot, a plugin can keep one and use it later as the condition for a control.
|
|
494
|
+
|
|
495
|
+
#### Conditional controls
|
|
496
|
+
|
|
497
|
+
**A control acts on the state the plugin saw.** `setPaused`, `seek`, `setRate`, `restart`, `close` and `media.capture` are conditional on the current `player.state` by default. If, after that state, the user or another plugin changed the source (opened something else, paused, jumped to another position), the call fails with `code: "stale"` and **does nothing**.
|
|
498
|
+
|
|
499
|
+
```text
|
|
500
|
+
plugin sees state #7 (movie.mp4 at 0:42)
|
|
501
|
+
user opens trailer.mp4 ─▶ state #8
|
|
502
|
+
plugin: seek(42) ─▶ rejected with "stale"; the trailer is not touched
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Without this rule, a "jump to bookmark" that arrives a moment late would seek a different video, and two plugins reacting to the same event would undo each other. With it, a late control is refused instead of applied to the wrong thing.
|
|
506
|
+
|
|
507
|
+
- The SDK refreshes `player.state` after each of its own operations, so a plugin's consecutive controls do not make each other stale.
|
|
508
|
+
- player.open is **unconditional** by default: an open the user asked for should replace whatever plays.
|
|
509
|
+
- To bind a control to an earlier state, keep that state and pass it as `ifCurrent` (Conditional):
|
|
510
|
+
|
|
511
|
+
```ts nocheck
|
|
512
|
+
// advance to the next item only if nobody changed the source after it ended
|
|
513
|
+
media.onEnd((ended) => plugin.player.open(next, { ifCurrent: ended }));
|
|
514
|
+
|
|
515
|
+
// capture the source the user saw before confirming, not whatever plays now
|
|
516
|
+
const seen = plugin.player.state;
|
|
517
|
+
const answer = await plugin.dialogs.confirm({ message: "Capture this frame?", confirm: "Capture" });
|
|
518
|
+
if (answer.confirmed) await plugin.media.capture({ ifCurrent: seen });
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
- isCurrent(state) answers the same question without acting, for example to warn in a form that the source changed meanwhile.
|
|
522
|
+
- `stale` is not an error to report loudly. It means the user did something else; usually the right reaction is to do nothing.
|
|
270
523
|
|
|
271
|
-
|
|
272
|
-
| ----------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
273
|
-
| `1.x` | `vestra/1` | Minor releases only add backwards-compatible API; patch releases fix documentation and bugs. |
|
|
274
|
-
| `1.1` | `vestra/1` | The additions need player 0.2.0: set `"minPlayerVersion": "0.2.0"` when you use them. |
|
|
275
|
-
| `1.3` | `vestra/1` | Menu ids (`@video`), `Theme.danger` and removing a video chain with `[]` need player 0.3.0: set `"minPlayerVersion": "0.3.0"` when you use them. |
|
|
276
|
-
| `1.4` | `vestra/1` | One exception to "only add": `ThemeId` lost the second palette and is `"vestra"` only. A plugin that compared with or set the removed id fails to type check; the player treats that id as `vestra`, so already built plugins keep running. |
|
|
277
|
-
| `1.5` | `vestra/1` | The light palette `vestra-light` and `@vestra/icons.slint` need player 0.4.0: set `"minPlayerVersion": "0.4.0"` when you use them. |
|
|
524
|
+
Two controls follow other rules because they are about the device or a gesture, not the source:
|
|
278
525
|
|
|
279
|
-
|
|
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.
|
|
528
|
+
|
|
529
|
+
#### Sources
|
|
530
|
+
|
|
531
|
+
##### Files and URLs
|
|
532
|
+
|
|
533
|
+
player.open(source) opens a path or URL (capability `playback.control`) and returns a Media handle for what this plugin opened. The promise resolves when the player accepted the open, not when it plays; media.ready() waits for playback to start and rejects with `failed` (it did not load), `replaced` (something else was opened first) or `ended`. onEnd reports each natural end.
|
|
534
|
+
|
|
535
|
+
A `Media` is not closed when its scope is disposed or the plugin stops: the user is watching it. media.close() closes it only if it is still the current source.
|
|
536
|
+
|
|
537
|
+
Every way of opening goes through `player.open`: the open dialog, a drop on the video area, a playlist, another plugin. That is what lets resolvers work for all of them.
|
|
538
|
+
|
|
539
|
+
##### Resolvers
|
|
540
|
+
|
|
541
|
+
A **resolver** (plugin.resolvers, capability `source.resolve`) lets a plugin take over opening some sources:
|
|
542
|
+
|
|
543
|
+
- A **web resolver** (`schemes` and exact `hosts`) turns a page URL into stream URLs the player can play (a video site's page into its media URLs, with headers).
|
|
544
|
+
- A **file resolver** (`extensions`, player 0.4.1) takes over local files by extension and returns a stream the plugin generates (an emulator for `.nes` files). It also needs `producer.av`.
|
|
545
|
+
|
|
546
|
+
```text
|
|
547
|
+
player.open("https://video.example/watch?v=1") from anyone: user, playlist, plugin
|
|
548
|
+
│ a resolver registered for https + video.example?
|
|
549
|
+
├── no ──▶ the player opens the URL itself
|
|
550
|
+
▼ yes
|
|
551
|
+
resolver.resolve(url, signal) in the resolver plugin's process
|
|
552
|
+
│ returns stream URLs (web) or a generated stream (file)
|
|
553
|
+
▼
|
|
554
|
+
the player opens the result under the original open:
|
|
555
|
+
same ifCurrent condition, same Media, same ready() for the caller
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
- **One owner per target.** One scheme and host, or one extension, has one resolver. A second plugin's registration fails with `conflict` but does not fail its activation; the player's built-in plugins register first.
|
|
559
|
+
- **The opener does not know.** The plugin that called `open` needs only `playback.control`; it gets its `Media`, `ready()` and errors as for any other source.
|
|
560
|
+
- **Cancellation is a signal.** The resolver's `signal` aborts when the open is replaced or closed, when it takes longer than 30 seconds, and when the resolver or its plugin goes away; a late answer is discarded.
|
|
561
|
+
- **A failure is the open's failure.** When `resolve` throws, the source goes to `phase: "error"` with the error's message as `state.error`; throw `plugin.i18n.error(…)` for a message in the user's language.
|
|
562
|
+
- resolvers.fileTypes lists every extension some plugin resolves, so a playlist or file filter can accept those files too.
|
|
563
|
+
|
|
564
|
+
##### Generated streams
|
|
565
|
+
|
|
566
|
+
player.openStream (capability `producer.av`) makes a source whose picture and sound the plugin generates: a signal generator, an emulator, a synthesizer. A file resolver returns the same kind of stream. The SDK calls the plugin's `frame()` on the audio clock (never concurrently, not while paused) and handles buffering, start-up and pausing; frames reach the player over a local connection the SDK sets up.
|
|
567
|
+
|
|
568
|
+
- `state.kind` is `"stream"`. It cannot seek, change speed or restart, and it **bypasses the filters**: the player does not decode it, so filter chains report `unsupported` while it plays (Video and audio processing).
|
|
569
|
+
- Frame sizes and sample values have limits; a frame that breaks them ends the stream with `invalid` (Reference: limits).
|
|
570
|
+
- Stream.onStop reports only stops from outside (another source, the user closing it, a failure), not the plugin's own `stop()`.
|
|
571
|
+
|
|
572
|
+
#### Next and previous
|
|
573
|
+
|
|
574
|
+
The player's **Previous** and **Next** (menu, keys, the bottom bar) are not tied to a playlist. One plugin at a time can **provide the sequence** with provideSequence, giving `next()`, `previous()` and two flags, `canNext` and `canPrevious`, which are bindings re-evaluated in every update pass.
|
|
575
|
+
|
|
576
|
+
- Every plugin sees the flags as `player.state.can.next` and `can.previous`, and player.next() asks the provider to move; the provider's `next` runs in its own scope.
|
|
577
|
+
- A second provider fails with `conflict` and does not fail activation; it is tried again when its flags change, so it takes over once the first provider goes away.
|
|
578
|
+
- Without a provider, or with the flag false, `next()` fails with `unavailable` and the menu items are disabled.
|
|
579
|
+
|
|
580
|
+
#### Core actions
|
|
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).
|
|
583
|
+
|
|
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
|
+
|
|
586
|
+
## Concept: Video and audio processing
|
|
587
|
+
|
|
588
|
+
A plugin can change what the user sees and hears in three different places. They look alike in the API (each is a handle with `set`, a `status` and `onStatus`), but they act at different stages and have different owners, so choosing the right one matters:
|
|
589
|
+
|
|
590
|
+
| | Changes | Owner | In snapshots and exports | API |
|
|
591
|
+
| --- | --- | --- | --- | --- |
|
|
592
|
+
| **Filter chain** | The decoded frames and samples: colour, blur, denoise, rotation, crop, volume, equalizer, delay… | Every plugin has its own chains; all are joined | Yes | VideoFilter, AudioFilter |
|
|
593
|
+
| **Display layer** | Where the video sits in the video area: aspect, fit, zoom, pan | One plugin at a time | No | VideoView |
|
|
594
|
+
| **Render options** | How the GPU renderer draws: HDR tone mapping, user shaders, motion interpolation | One plugin at a time | No | RenderOptions |
|
|
595
|
+
|
|
596
|
+
```text
|
|
597
|
+
decoder ─▶ plugin A's chain ─▶ plugin B's chain ─▶ … ─▶ (frameBlend) ─▶ renderer ─▶ video area
|
|
598
|
+
└──────────── filter chains: part of the picture ──────────┘ │ │
|
|
599
|
+
snapshots and exports use the same chain render options display layer
|
|
600
|
+
(display only) (display only)
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
All three need `processing.video` (or `processing.audio`) and `playback.read`; filter chains also need `events`, because the SDK must see each new source to apply the chain again.
|
|
604
|
+
|
|
605
|
+
#### Filter chains
|
|
606
|
+
|
|
607
|
+
plugin.video.filter(id) and `plugin.audio.filter(id)` give the plugin a chain of its own: a list of nodes (VideoNode, AudioNode) that the player turns into FFmpeg filters.
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
const picture = plugin.video.filter("picture");
|
|
611
|
+
picture.set([{ kind: "eq", brightness: 0.1, contrast: 1.1, saturation: 1, gamma: 1 }]);
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
- **A chain is a desired state.** `set(nodes)` says what the chain should be from now on; the last write wins. The SDK applies it to the current media, and **applies it again to every new source**, so the plugin does not react to file changes itself.
|
|
615
|
+
- **Each plugin owns its own chains.** The player joins the chains of all plugins in a stable order. Setting or disposing a chain changes only that plugin's contribution; an empty chain (`[]`, or for audio no nodes and no delay) withdraws it and leaves the others alone.
|
|
616
|
+
- **Nothing is sent until the first `set()`**, and disposing a chain that was applied withdraws it.
|
|
617
|
+
- **Limits are shared.** All plugins together have at most 64 nodes, one `frameBlend` and a bounded audio delay (Reference: limits); a value out of a node's range fails with `invalid`.
|
|
618
|
+
- **The same chain is the picture.** A processed snapshot or an export (plugin.media) goes through the same chains, so it matches what plays.
|
|
619
|
+
|
|
620
|
+
##### Status
|
|
621
|
+
|
|
622
|
+
A chain reports where it stands in FilterStatus:
|
|
623
|
+
|
|
624
|
+
```text
|
|
625
|
+
set(nodes)
|
|
626
|
+
│ no ready media ──▶ waiting ── media ready ──┐
|
|
627
|
+
▼ ▼
|
|
628
|
+
applying ──────────────────────────────────▶ applied
|
|
629
|
+
│
|
|
630
|
+
└──▶ failed (error.code) ── retry() / next set() / new source ──▶ applying
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
- `waiting` keeps the value until there is media to apply it to; without `events` the SDK never sees the media become ready, so the chain stays there.
|
|
634
|
+
- `applied` means the player accepted the chain, not that the next frame already shows it.
|
|
635
|
+
- `failed` is not retried by itself. Its `error.code` says why: `invalid` (a value out of range), `unsupported` (the source is a generated stream, which the player does not decode; the chain is applied again once a decoded source plays), `backend-uncertain` (the player's backend blocked that stage; call `retry()`).
|
|
636
|
+
|
|
637
|
+
##### Live parameters
|
|
638
|
+
|
|
639
|
+
A slider that drives a filter calls `set` on every move. That is fine: one request is in flight at a time and only the newest value is sent (Keep a slider responsive while dragging).
|
|
640
|
+
|
|
641
|
+
How smoothly the change shows depends on the field. Some fields, marked **live** on VideoNode and AudioNode (brightness and contrast, blur strength, hue, volume, equalizer gain…), change in place while playing. Changing any other field rebuilds that stage of the chain: the picture may stall for a frame and an audio filter's internal state starts over. Put the live fields on the controls users drag, and the others on switches and lists.
|
|
642
|
+
|
|
643
|
+
##### Ordering and side effects
|
|
644
|
+
|
|
645
|
+
- Video nodes act on the picture after the player has applied the container's rotation, so a portrait video is upright before a plugin's `transform` rotates it.
|
|
646
|
+
- `frameBlend` (frame-rate conversion by blending) always runs **last**, after every plugin's chain, wherever it is written, so temporal filters such as denoise and deinterlace still see the original frames. It changes the frame rate of exports too, and costs a lot of CPU: decide from `stats()` whether to offer it.
|
|
647
|
+
- Filters need frames in memory. While any plugin has a video chain, the player decodes in hardware with a copy back to memory instead of zero-copy decoding; when the last chain is removed it switches back. The decoder restarts at each switch, so the picture may pause briefly. stats().video.hwdec shows the decoder in use.
|
|
648
|
+
|
|
649
|
+
#### The display layer
|
|
650
|
+
|
|
651
|
+
plugin.video.view() returns the VideoView: aspect override, fit (keep with bars, stretch, fill and crop), zoom and pan. It changes only how the frames are placed in the video area. Snapshots, exports and filters do not see it.
|
|
652
|
+
|
|
653
|
+
- **One owner.** The first plugin to call `view()` owns the display layer. Another plugin's handle reports `failed` with `conflict` and tries again on each `set`, so it takes over once the owner disposes its view or stops. When the owner lets go, the player restores the defaults.
|
|
654
|
+
- **Writes merge.** `set({ zoom: 1.5 })` changes only the zoom; the other fields keep their values.
|
|
655
|
+
- **Values stay across sources**, as in mpv. A plugin that wants "reset zoom for every new file" does that itself when `player.state.sourceId` changes.
|
|
656
|
+
|
|
657
|
+
#### Render options
|
|
658
|
+
|
|
659
|
+
plugin.video.render() returns the RenderOptions of the GPU renderer: HDR to SDR tone mapping and the display's brightness, user shaders (`.glsl` files) and motion interpolation. Like the display layer they act at display time only, have one owner (independent of the display layer's owner) and merge writes.
|
|
660
|
+
|
|
661
|
+
- **Only the GPU renderer has them.** Under software rendering every `set` fails with `unsupported`; see Reference: platforms for where the player renders on the GPU. Treat `unsupported` as "hide these settings", not as an error.
|
|
662
|
+
- A shader that does not compile is reported later: the status first becomes `applied`, then `failed` with code `failed` and the compiler's first error line once mpv first uses the shader.
|
|
280
663
|
|
|
281
664
|
## Concept: Windows, panels, settings pages, dialogs and the OSD
|
|
282
665
|
|
|
@@ -284,7 +667,7 @@ A plugin can show its own user interface in five places. Three of them are drawn
|
|
|
284
667
|
|
|
285
668
|
It assumes you have written a first plugin (Getting started). For every method's parameters and errors, follow the links into the API reference.
|
|
286
669
|
|
|
287
|
-
|
|
670
|
+
#### The five surfaces
|
|
288
671
|
|
|
289
672
|
```text
|
|
290
673
|
┌─ main window ─────────────────────────────────────────┬─────────────┐
|
|
@@ -312,7 +695,7 @@ It assumes you have written a first plugin (Getting started). For every method's
|
|
|
312
695
|
|
|
313
696
|
**⑤ Dialogs** (plugin.dialogs, `ui.dialog`). The system's own dialogs: a confirmation alert (confirm) and the open-file, open-folder and save-file dialogs. The plugin passes text and gets an answer back; it does not draw anything. Only one dialog is open at a time, player-wide.
|
|
314
697
|
|
|
315
|
-
|
|
698
|
+
#### Choosing a surface
|
|
316
699
|
|
|
317
700
|
| You want to… | Use | Why not the others |
|
|
318
701
|
| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
@@ -322,19 +705,19 @@ It assumes you have written a first plugin (Getting started). For every method's
|
|
|
322
705
|
| Cover art or an idle screen while nothing is playing | Panel in the `backdrop` slot | It replaces the black video area; one plugin at a time. |
|
|
323
706
|
| A tool the user opens, uses and closes: picture adjustments, an inspector, an editor | Tool window | It has its own title bar and size and does not take space from the video. |
|
|
324
707
|
| Preferences that rarely change | Settings page | Users look for preferences in Options; the player keeps the window and its navigation. |
|
|
325
|
-
| A yes/no question before something destructive |
|
|
326
|
-
| A file or folder from the user |
|
|
708
|
+
| A yes/no question before something destructive | dialogs.confirm | Native look and keyboard handling (Enter, Escape); no Slint file. |
|
|
709
|
+
| A file or folder from the user | dialogs.openFile and friends | The system dialog is the only one users trust with paths. |
|
|
327
710
|
|
|
328
711
|
Two rules of thumb:
|
|
329
712
|
|
|
330
713
|
- **Prefer the smallest surface that works.** An OSD message before a dialog, a settings page before a tool window. Every window the user has to close is a cost.
|
|
331
714
|
- **Panels are shared space.** There is one sidebar, one bottom bar and one backdrop for all plugins. The sidebar holds several panels as tabs (the player draws the tab strip while there are two or more; showing, hiding and detaching apply to all of them). The bottom bar and the backdrop hold one: while one plugin's panel is there, another plugin's mount rejects with `conflict`, which does not fail its activation. Mount a panel only when the plugin's main purpose is to live there.
|
|
332
715
|
|
|
333
|
-
|
|
716
|
+
#### Life cycles
|
|
334
717
|
|
|
335
718
|
Every surface is a **handle** owned by the Scope that created it. Disposing the scope (or the plugin stopping) releases it: windows close, panels are unmounted, settings pages leave the Options window, the info block disappears. A plugin never has to clean up its interface by hand.
|
|
336
719
|
|
|
337
|
-
|
|
720
|
+
##### Tool window
|
|
338
721
|
|
|
339
722
|
```text
|
|
340
723
|
windows.create(file) nothing on screen yet; bind callbacks and properties now
|
|
@@ -346,7 +729,7 @@ Every surface is a **handle** owned by the Scope that created it. Disposing the
|
|
|
346
729
|
└──────────────┘ └──────────────┘ show() └──────────────┘
|
|
347
730
|
▲ │ │
|
|
348
731
|
│ close() │ │ the user: title-bar close button,
|
|
349
|
-
│ (no onClose) │ │
|
|
732
|
+
│ (no onClose) │ │ the standard close keys
|
|
350
733
|
└───────────────────────┘ │ → onClose listeners run
|
|
351
734
|
▲ │
|
|
352
735
|
└───────────────────────────────┘
|
|
@@ -358,7 +741,7 @@ Every surface is a **handle** owned by the Scope that created it. Disposing the
|
|
|
358
741
|
- `close()` and a user close both destroy the window instance. The handle stays usable: the next `show()` opens a **fresh** instance with the current properties (whatever the user typed into fields that the plugin did not record is gone).
|
|
359
742
|
- Only a user close calls onClose. Stop timers and polling there.
|
|
360
743
|
|
|
361
|
-
|
|
744
|
+
##### Panel
|
|
362
745
|
|
|
363
746
|
```text
|
|
364
747
|
panels.mount(slot, file)
|
|
@@ -380,7 +763,7 @@ Every surface is a **handle** owned by the Scope that created it. Disposing the
|
|
|
380
763
|
- The position (`plugin.panels.bottom`, `.sidebar`) is separate from the panel: it has the user's preference (`preferred`), whether it is on screen now (`effective`) and why not (`reason`), and its tabs (`panels`). Any plugin with `ui.mount` can read and toggle it, so a "Show playlist" command can live in a different plugin than the playlist. The panel's own state (Panel.state) adds whether its tab is the current one (`reason: "otherTab"` when not); Panel.toggle brings a background tab forward instead of closing the sidebar.
|
|
381
764
|
- The player remembers the sidebar's visibility, width, detached state and floating window position across launches.
|
|
382
765
|
|
|
383
|
-
|
|
766
|
+
##### Settings page
|
|
384
767
|
|
|
385
768
|
```text
|
|
386
769
|
settings.page(file, { id, category, title })
|
|
@@ -398,23 +781,23 @@ Every surface is a **handle** owned by the Scope that created it. Disposing the
|
|
|
398
781
|
- The Options window belongs to the player. The page exists from `ready` on, whether or not the window is open; property writes while it is closed are kept and applied when it opens.
|
|
399
782
|
- The page is compiled alone at registration, so a broken page fails its own `ready` and not the whole window.
|
|
400
783
|
|
|
401
|
-
|
|
784
|
+
##### Dialogs and the OSD
|
|
402
785
|
|
|
403
786
|
- Dialogs are one call each: the promise resolves when the user answers. There is no time limit and no way to close a dialog from code. Playback continues while it is open; command key bindings pause.
|
|
404
787
|
- The OSD message is fire-and-forget; a newer message (from any plugin) replaces the current one. The info block is a handle: `set(text)` shows or changes it, `set(null)` hides it, disposing removes it.
|
|
405
788
|
|
|
406
|
-
|
|
789
|
+
#### How Slint and TypeScript talk
|
|
407
790
|
|
|
408
791
|
The plugin process and the user interface run in different processes: your TypeScript runs in the plugin's Node.js process, the Slint files run inside the player. They exchange exactly two kinds of messages:
|
|
409
792
|
|
|
410
793
|
| Direction | Slint side | TypeScript side |
|
|
411
794
|
| ----------- | ---------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
412
|
-
| plugin → UI | `in property` or `in-out property` |
|
|
413
|
-
| UI → plugin | `callback name(args);` returning nothing |
|
|
795
|
+
| plugin → UI | `in property` or `in-out property` | set() or bind() |
|
|
796
|
+
| UI → plugin | `callback name(args);` returning nothing | on("name", handler) |
|
|
414
797
|
|
|
415
798
|
A tool window (Window), a panel (Panel) and a settings page (SettingsPage) all share this View surface. Nothing else crosses: the plugin cannot call Slint functions or read a property back; a value the user changes reaches the plugin only through a callback.
|
|
416
799
|
|
|
417
|
-
|
|
800
|
+
##### Where the properties live: component or global
|
|
418
801
|
|
|
419
802
|
- **A tool window file exports exactly one component**, usually `export component X inherits Window`. Its properties and callbacks are declared on that component. The window is yours, so the component is the window.
|
|
420
803
|
- **A panel or settings page file exports one component and one global.** The player composes your component into a window it owns (the main window, the floating sidebar window, the Options window), so the component instance is not a stable address: a new instance is made whenever the main window is rebuilt or the sidebar moves to or from its floating window. The **global** is the bridge: properties and callbacks are declared on it, the component reads and calls them (`ListBridge.rows`, `ListBridge.select(index)`), and the plugin talks to the global. The SDK finds the two names from the file's `export component` and `export global` lines.
|
|
@@ -462,15 +845,21 @@ export component ListPanel inherits Rectangle {
|
|
|
462
845
|
}
|
|
463
846
|
```
|
|
464
847
|
|
|
465
|
-
|
|
848
|
+
##### Rules for plugin Slint files
|
|
849
|
+
|
|
850
|
+
- **Exactly one exported component** per file (plus the one bridge global in a panel or settings page file). Helper components without `export` are allowed. The player appends a line that exports its palette to every plugin file, so do not declare anything named `VestraPalette`.
|
|
851
|
+
- **The `fluent-dark` style.** Every plugin file is compiled with Slint's `fluent-dark` style, so `std-widgets.slint` controls (`CheckBox`, `ComboBox`, `Switch`, `LineEdit`…) are dark; in the light palette the player switches them to their light colours (`Palette.color-scheme`) at run time. Do not set `Palette.color-scheme` yourself.
|
|
852
|
+
- **No fonts of your own.** Every window uses the system's interface font, and a character it lacks (CJK text in a Latin font, for example) falls back per character to a font for its script; on Windows the player sets that chain itself and ends it with a bundled CJK font. A plugin does not need to set a font; a `default-font-family` or `font-family` you set still applies, with the same fallback. The fonts per system are in Reference: theme.
|
|
853
|
+
- **Editor support through `libraryPaths`.** The `@vestra/…` imports resolve inside the player. For the Slint language server, point `libraryPaths` at the read-only copy in the SDK package: `{ "vestra": "node_modules/@vestra-player/sdk/slint/vestra" }`.
|
|
854
|
+
- **Text the user sees is `@tr("English")`**, translated when the player loads the file (Translate a plugin).
|
|
466
855
|
|
|
467
|
-
|
|
856
|
+
##### Names and values
|
|
468
857
|
|
|
469
858
|
- Write names in camelCase or kebab-case in TypeScript: the SDK turns every capital letter into `-` plus the lower-case letter (`canMark` → `can-mark`, `strengthChanged` → `strength-changed`). Fields inside struct values are **not** converted, so spell struct fields exactly as the Slint struct does (and without `_`, which Slint treats as `-`).
|
|
470
859
|
- Values must be JSON: booleans, numbers, strings, arrays (Slint models) and objects (Slint structs). Two exceptions: an image from ui.image, only as the **whole** value of an `image` property (nested in an array or object it is `invalid`), and a document from ui.markdown wherever the Slint type is `[MarkdownBlock]`: the whole value, or (SDK 1.5.4, the player after 0.4.0) a field at any depth, such as the `body` of each message in a chat list.
|
|
471
860
|
- Every name must exist in the Slint file. Writing a property the file does not declare makes a tool window's `show()` or a panel's `ready` fail, and in a settings page it keeps the Options window from opening; for a tool window, so does a callback bound with `on()` that the file does not declare. Once the view is open, such writes are reported to onError.
|
|
472
861
|
|
|
473
|
-
|
|
862
|
+
##### set, bind and the update pass
|
|
474
863
|
|
|
475
864
|
`set(props)` writes now. `bind(() => props)` declares that properties should _always_ equal a function of your state; the SDK re-evaluates every bound function in an **update pass** and sends only the values that changed.
|
|
476
865
|
|
|
@@ -481,17 +870,11 @@ Helper components without `export` are allowed in every file. The player appends
|
|
|
481
870
|
| Write fails | Retried with back-off (0.25 s doubling up to 30 s). | Not repeated until the value changes. |
|
|
482
871
|
| Typical use | One-off values: a document, a reset. | Everything derived from plugin state: labels, enabled flags, lists, status lines. |
|
|
483
872
|
|
|
484
|
-
Update passes run
|
|
485
|
-
|
|
486
|
-
- after every handler the SDK calls (a command's `run`, a view callback, an `onChange` listener) returns, and again when its promise settles;
|
|
487
|
-
- after player, subtitle, panel position, filter and export state changes;
|
|
488
|
-
- when you call plugin.update().
|
|
489
|
-
|
|
490
|
-
Several requests in the same microtask are merged into one pass, and each view has at most one write in flight: values that change meanwhile are merged into the next write. So binding a value that changes 60 times a second costs at most one write per round trip, never a queue.
|
|
873
|
+
Update passes run after every handler the SDK calls, after player and other state changes, and when you call plugin.update(); several requests are merged into one pass, and each view has at most one write in flight (The update pass has the details).
|
|
491
874
|
|
|
492
875
|
The rule to remember: **state you change after an `await` inside a handler appears only when the handler finishes**, unless something else triggers a pass. Call `plugin.update()` right after changing it (see Keep a slider responsive while dragging).
|
|
493
876
|
|
|
494
|
-
|
|
877
|
+
##### Two-way controls
|
|
495
878
|
|
|
496
879
|
A control whose value both the user and the plugin change (a slider, a switch, a text field, a combo box) needs a **two-way binding to an `in-out` property**:
|
|
497
880
|
|
|
@@ -506,7 +889,7 @@ Slider {
|
|
|
506
889
|
|
|
507
890
|
A one-way binding (`value: root.strength`) is broken by Slint the first time the user moves the control; after that the plugin's writes no longer move it. In a panel or settings page the same applies to the global: `checked <=> MyBridge.enabled`.
|
|
508
891
|
|
|
509
|
-
|
|
892
|
+
#### Theme and widgets
|
|
510
893
|
|
|
511
894
|
Plugin Slint files import the player's design tokens and widgets:
|
|
512
895
|
|
|
@@ -517,39 +900,12 @@ import { Button, Slider, ToolTipArea, UrlInputRow } from "@vestra/widgets.slint"
|
|
|
517
900
|
import { MarkdownView, MarkdownBlocks, MarkdownBlock } from "@vestra/markdown.slint";
|
|
518
901
|
```
|
|
519
902
|
|
|
520
|
-
- **Colours follow the player's palette.** The player has two palettes, `vestra` (dark, the default) and `vestra-light` (player 0.4.0, SDK 1.5), both drawn from the logo's colours
|
|
903
|
+
- **Colours follow the player's palette.** The player has two palettes, `vestra` (dark, the default) and `vestra-light` (player 0.4.0, SDK 1.5), both drawn from the logo's colours (see plugin.appearance). It sets the palette on every view before it appears and again when the user switches, so `Theme` colours change with it. Colours you hard-code do not. `Theme` properties are read-only.
|
|
521
904
|
- **The video area is dark in every palette** (`Theme.bg-video`). Text or icons you draw over the video (a backdrop, an overlay) use light colours, not `Theme.text`, which is dark in `vestra-light`.
|
|
522
|
-
-
|
|
523
|
-
-
|
|
524
|
-
- Control sizes and spacing: `button-height` 21px, `input-height` 21px, `form-row-gap` 26px, `dialog-padding` 14px, `radius-control` 4px (controls, rows, hover plates), `radius-panel` 8px (menus, panels) and others; the full list is the SDK's read-only copy in [`slint/vestra/theme.slint`](https://github.com/vestrasys/vestra-player-sdk/blob/main/slint/vestra/theme.slint). Player 0.4.0 rounded the corners (3px and 6px before).
|
|
525
|
-
- **Icons** (player 0.4.0, SDK 1.5): `Icon { name: "play"; color: Theme.text; }` draws one icon of the player's own set: round-capped 1.5px lines (1.25px for small glyphs), never filled, in the icon's own box (its default size; set `width` and `height` to scale). Names: `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`; `Icons.commands(name)`, `Icons.box-width(name)`, `Icons.box-height(name)` and `Icons.line(name)` give the path data for your own `Path`. Plugins that use it set `"minPlayerVersion": "0.4.0"`.
|
|
526
|
-
- Every plugin file is compiled with Slint's `fluent-dark` style, so `std-widgets.slint` controls (`CheckBox`, `ComboBox`, `Switch`, `LineEdit`…) are dark too; in `vestra-light` the player switches them to their light colours (`Palette.color-scheme`) at run time. Do not set `Palette.color-scheme` yourself.
|
|
527
|
-
- **Markdown** (`@vestra/markdown.slint`): `MarkdownView` scrolls one document; `MarkdownBlocks` (SDK 1.5.4, the player after 0.4.0) draws one without scrolling, as tall as its content, for your own layout or a `ListView` row. Both take `blocks: [MarkdownBlock]`, report `link-clicked(url)`, and with `code-copy: true` (default `false`) put a 复制 button on code blocks that reports `copy-code(code)`; copy it with plugin.clipboard. A chat window:
|
|
528
|
-
|
|
529
|
-
```slint
|
|
530
|
-
import { ListView } from "std-widgets.slint";
|
|
531
|
-
import { MarkdownBlocks, MarkdownBlock } from "@vestra/markdown.slint";
|
|
532
|
-
export struct Message { id: string, role: string, body: [MarkdownBlock] }
|
|
533
|
-
export component Chat inherits Window {
|
|
534
|
-
in property <[Message]> messages;
|
|
535
|
-
callback copy-code(string);
|
|
536
|
-
ListView {
|
|
537
|
-
for message in root.messages: VerticalLayout {
|
|
538
|
-
padding: 8px;
|
|
539
|
-
MarkdownBlocks {
|
|
540
|
-
blocks: message.body;
|
|
541
|
-
code-copy: true;
|
|
542
|
-
copy-code(code) => { root.copy-code(code); }
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
```
|
|
905
|
+
- **Use tokens, not values.** `Theme` has colour tokens (`text`, `accent`, `danger` for errors and destructive actions…), font sizes, control sizes and spacing; `Icon { name: "play"; color: Theme.text; }` draws an icon of the player's own set (player 0.4.0, SDK 1.5, so set `"minPlayerVersion": "0.4.0"`); `@vestra/widgets.slint` has the compact `Button`, `Slider`, `ToolTipArea` and `UrlInputRow`. Every token with its current values in both palettes, the icon names and the widgets' properties are in Reference: theme. The values may change between player versions; the tokens keep their meaning.
|
|
906
|
+
- **Markdown** (`@vestra/markdown.slint`): `MarkdownView` scrolls one document; `MarkdownBlocks` (SDK 1.5.4, the player after 0.4.0) draws one without scrolling, as tall as its content, for your own layout or a `ListView` row. Both take `blocks: [MarkdownBlock]`, report `link-clicked(url)`, and with `code-copy: true` (default `false`) put a **Copy** button on code blocks (`@tr("Copy")`, which the player translates into the interface language) that reports `copy-code(code)`; copy it with plugin.clipboard. A list of documents (a chat, one message per row) is the example at Ui.markdown; Example: Markdown shows one document in a window.
|
|
548
907
|
|
|
549
|
-
|
|
550
|
-
- For editor support, point the Slint language server's `libraryPaths` at the copy in the SDK package: `{ "vestra": "node_modules/@vestra-player/sdk/slint/vestra" }`.
|
|
551
|
-
|
|
552
|
-
### Sizes
|
|
908
|
+
#### Sizes
|
|
553
909
|
|
|
554
910
|
All sizes are logical pixels (Slint's `px`): one logical pixel is two physical pixels on a Retina display or at 200 % scaling.
|
|
555
911
|
|
|
@@ -564,25 +920,18 @@ All sizes are logical pixels (Slint's `px`): one logical pixel is two physical p
|
|
|
564
920
|
|
|
565
921
|
The operating system places a tool window when it opens; the player does not remember tool window positions or sizes, and the API cannot read or set them. To keep a tool window's _content_ across runs, see Remember a window's state between runs.
|
|
566
922
|
|
|
567
|
-
|
|
923
|
+
#### Focus and keyboard
|
|
568
924
|
|
|
569
925
|
- **Command key bindings belong to the main window.** A binding from CommandOptions.bindings fires while the main window or the floating sidebar window has focus (and the key was not taken by a focused control, such as Space in a text field). It does **not** fire while a tool window or the Options window has focus. Keys inside your own window are Slint's job: use a `FocusScope` with `key-pressed`, or a button's default action.
|
|
570
926
|
- A binding with `focus: "panel"` fires only while this plugin's panel (docked or floating) has focus, and wins over global bindings there.
|
|
571
|
-
- **Standard close keys are not bindings.**
|
|
927
|
+
- **Standard close keys are not bindings.** They close a tool window, the floating sidebar or the Options window like the title-bar button (`onClose` runs) before your Slint handlers or bindings see them; the keys per system are in Keys the system takes first.
|
|
572
928
|
- While a menu or a system dialog is open, bindings pause.
|
|
573
929
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
| | macOS | Windows |
|
|
577
|
-
| ---------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
578
|
-
| Close a tool window, floating sidebar, Options | ⌘W | Ctrl+W, Ctrl+F4 |
|
|
579
|
-
| `dialogs.confirm` | An `NSAlert` sheet on the active player window (the tool window that asked, usually). | A task dialog owned by the active player window, titled "Vestra Player"; `destructive` shows a warning icon; a `checkbox` is asked as a second yes/no dialog after the user confirms. |
|
|
580
|
-
| `Theme.font-mono` | Menlo | Consolas |
|
|
581
|
-
| `DropModifiers.cmd` | ⌘ | Ctrl |
|
|
930
|
+
#### Platform differences
|
|
582
931
|
|
|
583
|
-
The API is the same on
|
|
932
|
+
The API is the same on macOS and Windows; what differs is the system's own part: the standard close keys, how `dialogs.confirm` looks, the monospace font, the modifier `DropModifiers.cmd` stands for. Reference: platforms lists every difference. When the player runs without a user interface (headless, as in some automated setups), opening windows, mounting panels, dialogs, the OSD and `settings.show` fail with `unavailable`.
|
|
584
933
|
|
|
585
|
-
|
|
934
|
+
#### Common pitfalls
|
|
586
935
|
|
|
587
936
|
- **A window callback bound after the first `show()`.** It throws. Bind all callbacks right after `create()`; re-binding a name that already has a handler is fine.
|
|
588
937
|
- **A one-way binding on a control.** `value: root.x` stops following the plugin after the user touches the control. Use `in-out` and `<=>`.
|
|
@@ -595,34 +944,9 @@ The API is the same on both systems. Other systems answer `dialogs.confirm` with
|
|
|
595
944
|
- **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`).
|
|
596
945
|
- **Slot state without `events`.** PanelSlot.state, appearance.theme and their `onChange` listeners follow other plugins and the user only with the `events` capability.
|
|
597
946
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
`TestHost` from `@vestra-player/sdk/testing` runs a plugin without the player and records its interface: `host.window(id)` (`props`, `callbacks`, `visible`, `emit(callback, ...args)`, `userClose()`), `host.mounted()` for the panel, `host.settingsPage(id)` and `host.settings`, `host.osd` (messages shown), `host.confirms` and `host.dialogAnswers`. Property names in `props` are the Slint (kebab-case) names. The test host does not compile Slint files; check them by running the plugin in the player.
|
|
601
|
-
|
|
602
|
-
```ts
|
|
603
|
-
import assert from "node:assert/strict";
|
|
604
|
-
import test from "node:test";
|
|
605
|
-
import type { Plugin } from "@vestra-player/sdk";
|
|
606
|
-
import { startPlugin } from "@vestra-player/sdk/testing";
|
|
607
|
-
|
|
608
|
-
async function activate(plugin: Plugin) {
|
|
609
|
-
let count = 0;
|
|
610
|
-
const window = plugin.windows.create("ui/counter.slint");
|
|
611
|
-
window.bind(() => ({ label: `Clicked ${count} times` }));
|
|
612
|
-
window.on("clicked", () => count++);
|
|
613
|
-
await window.show();
|
|
614
|
-
}
|
|
947
|
+
#### Testing
|
|
615
948
|
|
|
616
|
-
|
|
617
|
-
const { host, stop } = await startPlugin(activate, {
|
|
618
|
-
capabilities: ["ui.window"],
|
|
619
|
-
});
|
|
620
|
-
host.window("counter").emit("clicked");
|
|
621
|
-
await host.settle();
|
|
622
|
-
assert.equal(host.window("counter").props["label"], "Clicked 1 times");
|
|
623
|
-
await stop();
|
|
624
|
-
});
|
|
625
|
-
```
|
|
949
|
+
`TestHost` from `@vestra-player/sdk/testing` runs a plugin without the player and records its interface: windows with their properties and callbacks, the mounted panel, settings pages, the OSD and dialogs. It does not compile Slint files; check them by running the plugin in the player. How to write such tests is in Test a plugin.
|
|
626
950
|
|
|
627
951
|
## How-to: Keep a slider responsive while dragging
|
|
628
952
|
|
|
@@ -630,7 +954,7 @@ test("the label counts clicks", async () => {
|
|
|
630
954
|
|
|
631
955
|
A slider reports every pointer movement, often more than 60 times a second. Each report is a callback in your plugin. The plugin stays responsive if every callback only **records the newest value and hands it to something that keeps only the newest value**. It stutters when every callback starts its own slow request and the requests queue up behind each other.
|
|
632
956
|
|
|
633
|
-
|
|
957
|
+
#### The complete example
|
|
634
958
|
|
|
635
959
|
A **Brightness** window with a slider from −1 to 1, a value label and a Reset button. The video follows the slider while it moves; an on-screen message reports the value when the user lets go.
|
|
636
960
|
|
|
@@ -738,196 +1062,1927 @@ export default async function activate(plugin: Plugin) {
|
|
|
738
1062
|
}
|
|
739
1063
|
```
|
|
740
1064
|
|
|
741
|
-
|
|
1065
|
+
#### Why this stays smooth
|
|
742
1066
|
|
|
743
1067
|
1. **The callback does not wait.** `brightnessChanged` updates a variable and calls `filter.set()`, which returns at once. The SDK runs the next callback as soon as it arrives.
|
|
744
1068
|
2. **The expensive part keeps only the newest value.** `filter.set()` has _latest-value_ semantics: while one request is in flight, newer values replace each other and only the last one is sent. A fast drag costs a request per round trip to the player, not one per pointer move, and the video ends exactly where the slider stopped.
|
|
745
1069
|
3. **The slider's value is bound, not set.** `bind` sends `brightness` only when it differs from what was sent last, and the player skips a write that equals what the slider already shows. So the plugin never fights the user's pointer, yet Reset (which changes the model) moves the thumb.
|
|
746
1070
|
4. **Work that should happen once happens on release.** The on-screen message (and anything like saving or logging) waits for `brightnessReleased`, which fires once per drag.
|
|
747
1071
|
|
|
748
|
-
The Slint side needs two things: an `in-out` property and a two-way binding (`value <=> root.brightness`). With a one-way binding (`value: root.brightness`) Slint breaks the binding the first time the user drags, and Reset stops moving the thumb.
|
|
1072
|
+
The Slint side needs two things: an `in-out` property and a two-way binding (`value <=> root.brightness`). With a one-way binding (`value: root.brightness`) Slint breaks the binding the first time the user drags, and Reset stops moving the thumb.
|
|
1073
|
+
|
|
1074
|
+
#### Which calls keep only the newest value
|
|
1075
|
+
|
|
1076
|
+
| Latest value wins (safe on every move) | One request per call (do not call on every move) |
|
|
1077
|
+
| ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1078
|
+
| VideoFilter`.set`, AudioFilter`.set` | player.seek: use player.scrub() and `Scrub.seek` for a seek slider |
|
|
1079
|
+
| player.setOutput (volume, mute) | `player.setRate` and other player controls |
|
|
1080
|
+
| `Scrub.seek` (about one seek per display frame) | osd.show: show a message on release |
|
|
1081
|
+
| View properties (`bind`, `set`), OsdPanel.set | `JsonStore.save` is debounced (300 ms), so it is fine; anything you write yourself is not |
|
|
1082
|
+
|
|
1083
|
+
#### When the slow part is your own code
|
|
1084
|
+
|
|
1085
|
+
If a move triggers your own asynchronous work (rendering a preview with `plugin.tools.run`, a network request), do not `await` it per callback: the SDK starts every callback without waiting for the previous one, so slow runs overlap and can finish out of order, leaving an _older_ result on screen. Give that work latest-value semantics yourself:
|
|
1086
|
+
|
|
1087
|
+
```ts
|
|
1088
|
+
import type { Plugin } from "@vestra-player/sdk";
|
|
1089
|
+
|
|
1090
|
+
/** Run `work` for the newest value only: one run at a time; values that arrive meanwhile replace each other. */
|
|
1091
|
+
export function latest<T>(work: (value: T) => Promise<void>) {
|
|
1092
|
+
let running = false;
|
|
1093
|
+
let next: { value: T } | undefined;
|
|
1094
|
+
return async (value: T) => {
|
|
1095
|
+
next = { value };
|
|
1096
|
+
if (running) return; // the running loop picks it up
|
|
1097
|
+
running = true;
|
|
1098
|
+
try {
|
|
1099
|
+
while (next) {
|
|
1100
|
+
const current = next.value;
|
|
1101
|
+
next = undefined;
|
|
1102
|
+
await work(current);
|
|
1103
|
+
}
|
|
1104
|
+
} finally {
|
|
1105
|
+
running = false;
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
declare function renderPreview(strength: number): Promise<string>; // your own slow work
|
|
1111
|
+
|
|
1112
|
+
export default async function activate(plugin: Plugin) {
|
|
1113
|
+
let preview = "";
|
|
1114
|
+
const window = plugin.windows.create("ui/preview.slint");
|
|
1115
|
+
window.bind(() => ({ preview }));
|
|
1116
|
+
const refresh = latest(async (strength: number) => {
|
|
1117
|
+
preview = await renderPreview(strength);
|
|
1118
|
+
plugin.update(); // changed after an await: send it now
|
|
1119
|
+
});
|
|
1120
|
+
window.on("strengthChanged", (strength: number) => refresh(strength));
|
|
1121
|
+
}
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
`plugin.update()` matters here. The SDK re-evaluates bound properties after a callback returns and again when its promise settles; a value you change **after an `await`** inside a longer task is otherwise shown only when that task ends. Call `plugin.update()` whenever such a task has something new to show.
|
|
1125
|
+
|
|
1126
|
+
## How-to: Remember a window's state between runs
|
|
1127
|
+
|
|
1128
|
+
**Goal:** a tool window comes back the way the user left it: its controls hold the last values, and if it was open when the player quit, it opens again at the next start.
|
|
1129
|
+
|
|
1130
|
+
A tool window keeps nothing by itself: closing it destroys the window instance, and the plugin process ends with the player. Keep the state in your plugin, store it with plugin.storage.json, show it with `bind`, and reopen the window during activation.
|
|
1131
|
+
|
|
1132
|
+
#### The complete example
|
|
1133
|
+
|
|
1134
|
+
The **Brightness** window from Keep a slider responsive while dragging, now remembering the brightness and whether the window was open. The manifest is the same as there, without `ui.osd`:
|
|
1135
|
+
|
|
1136
|
+
```json
|
|
1137
|
+
{
|
|
1138
|
+
"id": "examples.brightness",
|
|
1139
|
+
"version": "1.0.0",
|
|
1140
|
+
"apiVersion": "vestra/1",
|
|
1141
|
+
"entry": "main.ts",
|
|
1142
|
+
"minPlayerVersion": "0.3.0",
|
|
1143
|
+
"capabilities": [
|
|
1144
|
+
"commands",
|
|
1145
|
+
"ui.window",
|
|
1146
|
+
"processing.video",
|
|
1147
|
+
"playback.read",
|
|
1148
|
+
"events"
|
|
1149
|
+
]
|
|
1150
|
+
}
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
`ui/brightness.slint` is unchanged: it declares `in-out property <float> brightness`, `in property <string> label` and the callbacks `brightness-changed(float)`, `brightness-released(float)` and `reset()` (the full file is on the slider page).
|
|
1154
|
+
|
|
1155
|
+
`main.ts`:
|
|
1156
|
+
|
|
1157
|
+
```ts
|
|
1158
|
+
import type { Plugin } from "@vestra-player/sdk";
|
|
1159
|
+
|
|
1160
|
+
/** Everything the plugin remembers, in <dataDirectory>/brightness.json. */
|
|
1161
|
+
interface Saved {
|
|
1162
|
+
/** −1–1, 0 = unchanged. */
|
|
1163
|
+
brightness: number;
|
|
1164
|
+
/** The window was open when the player quit. */
|
|
1165
|
+
open: boolean;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const DEFAULTS: Saved = { brightness: 0, open: false };
|
|
1169
|
+
|
|
1170
|
+
/** A missing, broken or outdated file falls back to DEFAULTS. */
|
|
1171
|
+
function isSaved(value: unknown): value is Saved {
|
|
1172
|
+
const v = value as Partial<Saved> | null;
|
|
1173
|
+
return (
|
|
1174
|
+
typeof v === "object" &&
|
|
1175
|
+
v !== null &&
|
|
1176
|
+
typeof v.brightness === "number" &&
|
|
1177
|
+
Math.abs(v.brightness) <= 1 &&
|
|
1178
|
+
typeof v.open === "boolean"
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
export default async function activate(plugin: Plugin) {
|
|
1183
|
+
// 1. Load: read once, synchronously; defaults if there is no valid file.
|
|
1184
|
+
const store = plugin.storage.json<Saved>("brightness", {
|
|
1185
|
+
defaults: DEFAULTS,
|
|
1186
|
+
validate: isSaved,
|
|
1187
|
+
});
|
|
1188
|
+
/** Change part of the state; the file is written 300 ms after the last change. */
|
|
1189
|
+
const remember = (patch: Partial<Saved>) =>
|
|
1190
|
+
store.save({ ...store.value, ...patch });
|
|
1191
|
+
|
|
1192
|
+
const filter = plugin.video.filter("brightness");
|
|
1193
|
+
const apply = () =>
|
|
1194
|
+
filter.set([
|
|
1195
|
+
{
|
|
1196
|
+
kind: "eq",
|
|
1197
|
+
brightness: store.value.brightness,
|
|
1198
|
+
contrast: 1,
|
|
1199
|
+
saturation: 1,
|
|
1200
|
+
gamma: 1,
|
|
1201
|
+
},
|
|
1202
|
+
]);
|
|
1203
|
+
apply(); // the effect is back before the window is
|
|
1204
|
+
|
|
1205
|
+
// 2. Show: the window always displays the stored state.
|
|
1206
|
+
const window = plugin.windows.create("ui/brightness.slint");
|
|
1207
|
+
window.bind(() => ({
|
|
1208
|
+
brightness: store.value.brightness,
|
|
1209
|
+
label: store.value.brightness.toFixed(2),
|
|
1210
|
+
}));
|
|
1211
|
+
window.on("brightnessChanged", (value: number) => {
|
|
1212
|
+
remember({ brightness: Math.min(1, Math.max(-1, value)) });
|
|
1213
|
+
apply();
|
|
1214
|
+
});
|
|
1215
|
+
window.on("reset", () => {
|
|
1216
|
+
remember({ brightness: 0 });
|
|
1217
|
+
apply();
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// 3. Track whether it is open: only the user closing it counts as "closed".
|
|
1221
|
+
window.onClose(() => remember({ open: false }));
|
|
1222
|
+
const open = async () => {
|
|
1223
|
+
await window.show();
|
|
1224
|
+
remember({ open: true });
|
|
1225
|
+
};
|
|
1226
|
+
plugin.commands.add({
|
|
1227
|
+
id: "brightness",
|
|
1228
|
+
path: ["@video", "Brightness…"],
|
|
1229
|
+
run: open,
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
// 4. Restore: reopen it if it was open when the player quit.
|
|
1233
|
+
if (store.value.open) await open().catch(() => remember({ open: false }));
|
|
1234
|
+
}
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
#### How it works
|
|
1238
|
+
|
|
1239
|
+
- **Load once, at activation.** `storage.json` reads `<dataDirectory>/brightness.json` synchronously when it is created. A file that cannot be parsed or fails `validate` is renamed to `brightness.json.bad-<timestamp>` and `defaults` is used, so a damaged or older file never breaks the plugin. Always pass a `validate` function: the file is outside your control.
|
|
1240
|
+
- **Bind the stored value, not a copy.** `window.bind` reads `store.value` in every update pass, so whatever changes the state (a callback, Reset, a command) updates the window, and a window opened later starts with the current values.
|
|
1241
|
+
- **Save on every change; the store debounces.** `save()` replaces `store.value` at once and writes the file 300 ms after the last call (a temporary file, then a rename). Dragging the slider writes the file once, after the drag. When the plugin stops (the player quits, the user disables the plugin), the store is flushed, so the last value is not lost.
|
|
1242
|
+
- **"Open" means "the user did not close it".** `onClose` runs only when the user closes the window (the title-bar button or the standard close keys). When the player quits, the plugin stops and its windows close _without_ `onClose`, so `open` stays `true` and the window comes back next time. That is the behaviour users expect from an application's tool windows.
|
|
1243
|
+
- **Restore after binding.** The window is created and bound before `open()` runs, so it opens with the stored values and without flashing defaults. If it cannot open (for example in a player without a user interface), the plugin forgets that it was open instead of failing activation.
|
|
1244
|
+
|
|
1245
|
+
#### What you cannot restore
|
|
1246
|
+
|
|
1247
|
+
- **Position and size.** The operating system places a tool window when it opens. The player does not remember tool window frames, and the API has no way to read or set them; the window's size comes from its Slint file (`width`, `preferred-width` and so on).
|
|
1248
|
+
- **State that only Slint knows.** Text in a field, a scroll position or a selected tab survives only if a callback reports it to the plugin and the plugin stores it and writes it back as a property, as `brightness` does here.
|
|
1249
|
+
|
|
1250
|
+
#### Variations
|
|
1251
|
+
|
|
1252
|
+
- **Several windows from one file** (one per document, created with `windows.create(file, { id })`): store an object keyed by the window id and restore each one.
|
|
1253
|
+
- **Preferences rather than state:** a choice such as "reopen my windows at startup" belongs on a settings page (plugin.settings.page) that reads and writes the same store (the `SettingsPage` reference has an example).
|
|
1254
|
+
- **Large or binary data:** write your own files in plugin.dataDirectory (to a temporary name, then rename) and keep only small settings in `storage.json`.
|
|
1255
|
+
|
|
1256
|
+
## How-to: Translate a plugin
|
|
1257
|
+
|
|
1258
|
+
Vestra Player (0.4.3 or later) shows its interface in one of seven languages. The user picks one in **Settings ▸ Language** (or follows the system); the choice takes effect at the next start, and the language stays the same for the whole run. A plugin joins in by shipping one gettext file per language. It writes **no translation code**: the player translates.
|
|
1259
|
+
|
|
1260
|
+
The Counter example is a complete translated plugin. Moving an existing plugin over is in Migrate a plugin to English source text.
|
|
1261
|
+
|
|
1262
|
+
#### The rule: English source text in three places
|
|
1263
|
+
|
|
1264
|
+
Every text a user can see is written in English, in one of three places, and the player looks it up in the plugin's catalog for the interface language. Untranslated text shows in English.
|
|
1265
|
+
|
|
1266
|
+
1. **Declarations.** What the plugin declares, the player shows translated:
|
|
1267
|
+
|
|
1268
|
+
```ts nocheck
|
|
1269
|
+
plugin.commands.add({
|
|
1270
|
+
id: "counter",
|
|
1271
|
+
path: ["Tools", "Counter…"], // English: menus merge by these words
|
|
1272
|
+
run: () => window.show(),
|
|
1273
|
+
});
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
Command titles and paths, settings page titles (`settings.page`), panel and tab titles (`panels.mount`), command `reason`s, and the manifest's `name`, `punchline`, `description` and `tags`. Menus of different plugins merge by the English words, so plugins that share a submenu must write it the same way; the player's own menus are `Open`, `Playback`, `Subtitles`, `Video`, `Audio`, `Screen`, `Settings` (or `@video` and the other ids) and show in the player's words.
|
|
1277
|
+
|
|
1278
|
+
2. **Slint.** A window's texts belong in its `.slint` file, wrapped in `@tr`:
|
|
1279
|
+
|
|
1280
|
+
```slint
|
|
1281
|
+
title: @tr("Counter");
|
|
1282
|
+
Text { text: @tr("{n} click" | "{n} clicks" % root.count); }
|
|
1283
|
+
Button { text: @tr("Add {}", root.step); }
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
TypeScript passes data (`count`, `step`), never finished sentences. `@tr` takes `{}`, `{0}`, `{1}` arguments and a plural form (`"one" | "other" % n`, where `{n}` is the number). The player has two plural forms: one for `n == 1`, the other for every other number. A context (`@tr("Menu" => "Open")`) tells two meanings of one English word apart.
|
|
1287
|
+
|
|
1288
|
+
3. **Sentences the plugin hands the player**: `osd.show`, `OsdPanel.set` and `dialogs.confirm` take a template and its arguments:
|
|
1289
|
+
|
|
1290
|
+
```ts nocheck
|
|
1291
|
+
await plugin.osd.show("Speed {}×", [rate]);
|
|
1292
|
+
await plugin.dialogs.confirm({
|
|
1293
|
+
message: "Delete {} bookmarks?",
|
|
1294
|
+
confirm: "Delete",
|
|
1295
|
+
args: [count],
|
|
1296
|
+
});
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
Never build the sentence in TypeScript (`` `Speed ${rate}×` ``, `"Speed " + rate`): a translator can only translate a whole template, and word order differs between languages. Named arguments work too: `osd.show("{name} is ready", { name })`.
|
|
1300
|
+
|
|
1301
|
+
`plugin.i18n.locale` tells which language this run uses, for what is not text (a date: `date.toLocaleDateString(plugin.i18n.locale)`). There is no `t()` function and no event when the language changes: it never changes while the player runs.
|
|
1302
|
+
|
|
1303
|
+
#### Text that mixes words and data
|
|
1304
|
+
|
|
1305
|
+
Some text is neither a fixed declaration nor a window Slint can build: a menu item "Track 2 (opus)", a ComboBox model whose first item is "No Audio" and the rest track names, an OSD message whose reason is itself a sentence, a list of file names. For these, make a **text value** with `plugin.i18n.text(source, args)` (SDK 1.5.11, player 0.4.3) and pass it where a string would go. It is plain data, not a translation: the player translates `source` with the plugin's catalog and fills in the arguments when it shows it.
|
|
1306
|
+
|
|
1307
|
+
```ts nocheck
|
|
1308
|
+
const { text, list } = plugin.i18n;
|
|
1309
|
+
|
|
1310
|
+
// A menu item with data (the menu merges by the English "Track 2 (opus)").
|
|
1311
|
+
plugin.commands.add({
|
|
1312
|
+
id: `track-${n}`,
|
|
1313
|
+
path: ["@audio", "Audio Track", text("Track {} ({})", [n, codec])],
|
|
1314
|
+
run,
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
// A [string] model: fixed words and data in one list.
|
|
1318
|
+
picker.set({ labels: [text("No Audio"), ...tracks.map((t) => text("Track {} · {}", [t.id, t.title]))] });
|
|
1319
|
+
|
|
1320
|
+
// An argument that is a sentence of its own.
|
|
1321
|
+
await plugin.osd.show("Download failed: {}", [text("Connection timed out")]);
|
|
1322
|
+
|
|
1323
|
+
// Items joined the language's way: "a.glsl, b.glsl", "a.glsl、b.glsl".
|
|
1324
|
+
await plugin.osd.show("Removed missing shaders: {}", [list(missing)]);
|
|
1325
|
+
|
|
1326
|
+
// A message of several parts, each translated: "Added: 3, duplicates skipped: 2".
|
|
1327
|
+
await plugin.osd.show(list([text("Added: {}", [added]), text("duplicates skipped: {}", [skipped])], "clauses"));
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
Arguments are data (strings and numbers, shown as they are) unless they are text values themselves, which are translated too, to any depth (8 levels). `list(items, style)` joins with the interface language's separator: `"list"` (`, ` / `、`), `"clauses"` (`, ` / `,`), `"semicolons"`, `"sentences"` (`. ` / `。`) or `"lines"`.
|
|
1331
|
+
|
|
1332
|
+
A text value is accepted wherever the player shows a plugin's text:
|
|
1333
|
+
|
|
1334
|
+
| Where | Notes |
|
|
1335
|
+
| --- | --- |
|
|
1336
|
+
| `commands.add` `title`, `path` segments, `reason`; `Command.set({ reason })` | not a `list`; the menu merges by the English rendering |
|
|
1337
|
+
| `osd.show(value)`, `OsdPanel.set(value)`, and as their `args` | a text value as the message carries its own args |
|
|
1338
|
+
| `dialogs.confirm` `message`, `detail`, `confirm`, `cancel`, `checkbox` | `args` fills only the string ones |
|
|
1339
|
+
| file dialog `title`; `saveFile` `fileName` | a string `fileName` is data; `text("Playlist.m3u8")` is translated |
|
|
1340
|
+
| window, panel and settings page properties, where Slint has `string` | at any depth: `[string]` models, struct fields |
|
|
1341
|
+
| `TimelineLayer.set` mark titles, `subtitles.load({ title })` | a plain string stays data |
|
|
1342
|
+
| a resolver's failure: `throw plugin.i18n.error("The ROM file was not found.")` | shown translated as the open's failure and `player.state.error` |
|
|
1343
|
+
|
|
1344
|
+
`extract` collects the `source` of every `text(…)` and `error(…)` call (literals, `?:` branches, `as const` tables), whether called as `plugin.i18n.text` or destructured. Write the source as a literal, never `` text(`Track ${n}`) ``. A source made only of placeholders and punctuation (`"{} ({})"`, `"{0}: {1}"`) is collected like any other: languages differ in punctuation too (Chinese writes `"{}({})"`, `"{0}:{1}"`), and a translator who keeps it as it is simply copies it.
|
|
1345
|
+
|
|
1346
|
+
**Text that leaves the player.** What the plugin copies to the clipboard or writes to a file is not shown by the player. For that only, `plugin.i18n.resolve(value)` returns the translated string (with the same catalog the player uses): `clipboard.writeText(resolve(list(lines, "lines")))`. Never use it for the interface: a string resolved in TypeScript and then shown would hide the sentence from `check`, and the interface takes text values directly.
|
|
1347
|
+
|
|
1348
|
+
**OSD panel lines that must fit.** `OsdPanel.set(value, args, { maxColumns })` cuts each line after translation (a Chinese, Japanese or Korean character is two columns), so the plugin does not measure translated text.
|
|
1349
|
+
|
|
1350
|
+
#### Languages
|
|
1351
|
+
|
|
1352
|
+
The player's interface languages, and the locale code that names a language's catalog file and is `plugin.i18n.locale` in a run:
|
|
1353
|
+
|
|
1354
|
+
| Code | Language | Shown in the menu as |
|
|
1355
|
+
| --- | --- | --- |
|
|
1356
|
+
| `en` | English (source language) | English |
|
|
1357
|
+
| `zh-Hans` | Simplified Chinese | 简体中文 |
|
|
1358
|
+
| `ja` | Japanese | 日本語 |
|
|
1359
|
+
| `ko` | Korean | 한국어 |
|
|
1360
|
+
| `de` | German | Deutsch |
|
|
1361
|
+
| `fr` | French | Français |
|
|
1362
|
+
| `es` | Spanish | Español |
|
|
1363
|
+
|
|
1364
|
+
A plugin may ship any subset; a `.po` file named after a code not in this table is reported by `i18n check` and never read by the player.
|
|
1365
|
+
|
|
1366
|
+
#### Files
|
|
1367
|
+
|
|
1368
|
+
```text
|
|
1369
|
+
locales/
|
|
1370
|
+
messages.pot every text to translate (written by vestra-plugin i18n extract)
|
|
1371
|
+
zh-Hans.po Simplified Chinese
|
|
1372
|
+
de.po German …
|
|
1373
|
+
```
|
|
1374
|
+
|
|
1375
|
+
One `<lang>.po` per language you provide, named by its locale (the codes in Languages; English is the source and has none). They are ordinary gettext files: any `.po` editor (Poedit, Lokalize, Weblate, a text editor) works. `vestra-plugin build` compiles them into `dist/locales/<lang>.json`, which the player reads; during development (`--plugin <dir>`) the player reads the `.po` files directly.
|
|
1376
|
+
|
|
1377
|
+
#### Commands
|
|
1378
|
+
|
|
1379
|
+
```sh
|
|
1380
|
+
vestra-plugin i18n extract # write locales/messages.pot, update every locales/<lang>.po
|
|
1381
|
+
vestra-plugin i18n extract --locale de # start a new language
|
|
1382
|
+
vestra-plugin i18n check # coverage and problems; fails on stale catalogs
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
`extract` finds every `@tr` in the `.slint` files, the texts given to the calls above (literals, both branches of a `?:`, and `as const` tables, through the TypeScript type checker), and the manifest's display fields. It keeps existing translations, adds new texts untranslated, and moves texts that disappeared to the end of each `.po` as obsolete (`#~`), so a text that comes back keeps its translation. Comments you write yourself stay where they are (SDK 1.5.13): lines before the header (`# Reviewed by … on 2026-10-25`, `# Translator: …`) and `# ` comments on an entry. A plural entry gets exactly as many `msgstr[n]` lines as the file's `Plural-Forms` has `nplurals`: one for Japanese, Korean and Simplified Chinese (an empty `msgstr[1]` left from an older SDK is removed), two for the others.
|
|
1386
|
+
|
|
1387
|
+
`check` reports per language how much is translated, translations that lost or changed a placeholder (keep every `{…}` exactly as in the English; reorder with `{0}`, `{1}` only where the English has them), plural forms missing, a stale `messages.pot`, and text that cannot be translated: a sentence composed in TypeScript, a value that is not a literal, Chinese or other non-English source text, and Slint text without `@tr`. These are warnings, except that `i18n check` fails (exit code 1, SDK 1.5.12) when `messages.pot` is out of date or a `.po` lacks some of its messages: run `extract` and commit the result, so CI catches a forgotten `extract`. `vestra-plugin check` prints the same lines when the plugin has a `locales/` folder; none of them fails `check` or a build.
|
|
1388
|
+
|
|
1389
|
+
A note for translators goes in a comment that starts with `TRANSLATORS:` right before the text (in TypeScript on the property or the call, or on an entry of an `as const` table (SDK 1.5.12); in Slint on the line above the `@tr`); `extract` copies it into the files.
|
|
1390
|
+
|
|
1391
|
+
```ts nocheck
|
|
1392
|
+
const REASONS = {
|
|
1393
|
+
// TRANSLATORS: the download took longer than 30 seconds.
|
|
1394
|
+
timeout: "Connection timed out",
|
|
1395
|
+
dns: "Server not found",
|
|
1396
|
+
} as const;
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
#### Machine translations and review
|
|
1400
|
+
|
|
1401
|
+
A translation made by a machine (or a language model) is marked as such until a person who knows the language has checked it, so that a reader of the file can tell which entries nobody has looked at yet.
|
|
1402
|
+
|
|
1403
|
+
- **Flag each machine-translated entry `x-machine`**, in the entry's `#,` line before its `msgid` (next to `fuzzy`, separated by a comma: `#, fuzzy, x-machine`):
|
|
1404
|
+
|
|
1405
|
+
```po
|
|
1406
|
+
#. The button that adds one to the count.
|
|
1407
|
+
#: ui/counter.slint:12
|
|
1408
|
+
#, x-machine
|
|
1409
|
+
msgid "Add {}"
|
|
1410
|
+
msgstr "{} hinzufügen"
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
The flag is only for people. The player and the SDK read `fuzzy` alone (a fuzzy entry is left out of the catalog) and ignore every other flag, so an `x-machine` entry is compiled and shown like any other. `i18n extract` keeps the flag with the translation, also when the text becomes obsolete (`#~`).
|
|
1414
|
+
- **A review of the whole file goes in a header comment**: a line before the `msgid ""` header, such as `# Reviewed by an AI reviewer on 2026-10-25` or `# Reviewed by Jane Doe on 2026-10-25 (menus and settings)`. `extract` keeps lines before the header (SDK 1.5.13). An automatic review does not replace a human one: it leaves the `x-machine` flags in place.
|
|
1415
|
+
- **A human reviewer removes the flag.** Someone who knows the language reads each entry, corrects what is wrong and then deletes that entry's `x-machine` (from `#, fuzzy, x-machine` only `x-machine`; remove the `#,` line when nothing is left). An entry still flagged has not been reviewed: `grep -c x-machine locales/de.po` counts what is left. When the whole file is done, the header comment names the reviewer and the date.
|
|
1416
|
+
|
|
1417
|
+
Translations written by a person from the start carry no flag.
|
|
1418
|
+
|
|
1419
|
+
#### Tests
|
|
1420
|
+
|
|
1421
|
+
`startPlugin(activate, { directory, locale: "zh-Hans" })` loads `locales/zh-Hans.po` and makes the test host show what a user of that language sees:
|
|
1422
|
+
|
|
1423
|
+
```ts nocheck
|
|
1424
|
+
const { host, plugin, stop } = await startPlugin(activate, { directory, locale: "zh-Hans" });
|
|
1425
|
+
assert.equal(plugin.i18n.locale, "zh-Hans");
|
|
1426
|
+
assert.deepEqual(host.command("counter").display.path, ["工具", "计数器…"]);
|
|
1427
|
+
assert.deepEqual(host.osd, ["计数:1"]); // osd.show("Count: {}", [1])
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
`command.display` is the menu as shown (`display.reason` the shown reason), `host.osd`, `host.osdPanel` and `host.confirmTexts` the shown messages, `host.fileDialogs` the file dialogs' titles and file names, window and panel `props` the property values with their text values shown, `host.translate(text, args)` any text (a string or a text value). The declared English stays in `command.declaration` and `host.sent(...)`. Without `locale`, the plugin's own texts show in English as declared, but the player's own texts (core action OSD messages, menu titles and reasons) stay in Simplified Chinese, as in SDK 1.5.8 and older, so existing tests keep passing; pass `locale: "en"` to see the player in English.
|
|
1431
|
+
|
|
1432
|
+
#### Limits
|
|
1433
|
+
|
|
1434
|
+
- The language changes only at the next start of the player.
|
|
1435
|
+
- Plurals have two forms (1 and everything else). For a language with more forms (Russian, Arabic, Polish) the player does not ship a translation.
|
|
1436
|
+
- Numbers in templates print as JavaScript writes them (`1.5`), not in the language's own format.
|
|
1437
|
+
- A text the player cannot find in the catalog shows in English; so does a plugin's text on a player before 0.4.3, which also shows templates without filling them in. Set `"minPlayerVersion": "0.4.3"` in the manifest of a plugin that relies on templates or text values (older players run an older SDK, which has no `plugin.i18n.text`).
|
|
1438
|
+
- Text values have limits on arguments, list items, nesting and bytes (Text values); a property value's limits (Property values) count a text value as the string it becomes.
|
|
1439
|
+
|
|
1440
|
+
## How-to: Migrate a plugin to English source text
|
|
1441
|
+
|
|
1442
|
+
For plugins written before SDK 1.5.9 whose texts are in one language (Chinese, for the plugins built into Vestra Player). After the migration every text is English in the source, and the old texts become the first translation. The behaviour stays the same: a user with the same language sees exactly the texts as before. Read Translate a plugin first; this page is the checklist.
|
|
1443
|
+
|
|
1444
|
+
Needs player 0.4.3 or later: set `"minPlayerVersion": "0.4.3"` in `manifest.json`.
|
|
1445
|
+
|
|
1446
|
+
#### 1. Menus: English paths
|
|
1447
|
+
|
|
1448
|
+
Before:
|
|
1449
|
+
|
|
1450
|
+
```ts nocheck
|
|
1451
|
+
plugin.commands.add({ id: "snapshot", path: ["视频", "截图", "保存当前画面…"], run: save });
|
|
1452
|
+
```
|
|
1453
|
+
|
|
1454
|
+
After:
|
|
1455
|
+
|
|
1456
|
+
```ts nocheck
|
|
1457
|
+
plugin.commands.add({ id: "snapshot", path: ["@video", "Snapshot", "Save Current Frame…"], run: save });
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
- The player's menus: `@open`, `@playback`, `@subtitles`, `@video`, `@audio`, `@screen`, `@settings` (or their keys `Open`, `Playback`, `Subtitles`, `Video`, `Audio`, `Screen`, `Settings`). Its own submenus `Playback ▸ Speed` and `Playback ▸ Seek` keep the player's words too. The old Chinese titles of these menus (视频, 播放 ▸ 速度 …) still work, so an unmigrated plugin lands in the same menus meanwhile.
|
|
1461
|
+
- **Shared submenus merge by the English words.** When another plugin puts items into the same submenu, both must write it identically. Submenu keys in use lists the English keys the player and its built-in plugins use; reuse those, and ask before inventing a new shared one.
|
|
1462
|
+
- Title case for menu items ("Save Current Frame…"), an ellipsis `…` when the item opens a window or asks something.
|
|
1463
|
+
- Dynamic items (tracks, devices, presets) whose titles are data (a file name, a device name) stay data: they are not translated. Items whose titles come from a fixed list are text: put the list in an `as const` table so `extract` finds them:
|
|
1464
|
+
|
|
1465
|
+
```ts nocheck
|
|
1466
|
+
const RATES = [
|
|
1467
|
+
{ id: "half", title: "0.5×" },
|
|
1468
|
+
{ id: "double", title: "2×" },
|
|
1469
|
+
] as const;
|
|
1470
|
+
```
|
|
1471
|
+
|
|
1472
|
+
- `order`, `group`, `radio`, `bindings` do not change. Saved user key bindings are kept: they are stored by plugin and command id, not by path.
|
|
1473
|
+
|
|
1474
|
+
#### 2. Slint: `@tr`
|
|
1475
|
+
|
|
1476
|
+
Before:
|
|
1477
|
+
|
|
1478
|
+
```slint
|
|
1479
|
+
Button { text: "恢复默认"; }
|
|
1480
|
+
Text { text: root.count + " 个文件"; }
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1483
|
+
After:
|
|
1484
|
+
|
|
1485
|
+
```slint
|
|
1486
|
+
Button { text: @tr("Restore Defaults"); }
|
|
1487
|
+
Text { text: @tr("{n} file" | "{n} files" % root.count); }
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1490
|
+
Wrap every literal a user reads (`text`, `title`, `placeholder-text`, `tip`, default values of properties shown as text). Do not concatenate in Slint either: `"[" + a + "]"` becomes `@tr("[{}]", a)`.
|
|
1491
|
+
|
|
1492
|
+
#### 3. Sentences built in TypeScript
|
|
1493
|
+
|
|
1494
|
+
The plugin may no longer build a user-visible sentence in TypeScript. Three ways out, in this order of preference:
|
|
1495
|
+
|
|
1496
|
+
**Into the Slint file.** When a window shows it, let the window build it from data:
|
|
1497
|
+
|
|
1498
|
+
```ts nocheck
|
|
1499
|
+
// Before: TypeScript composes the tooltip.
|
|
1500
|
+
panel.bind(() => ({ tipBack: `后退 ${step} 秒 (${key})` }));
|
|
1501
|
+
// After: TypeScript passes data; Slint has text: @tr("Back {} s ({})", root.step, root.key)
|
|
1502
|
+
panel.bind(() => ({ step, key }));
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
**A template with arguments**, for the OSD and dialogs:
|
|
1506
|
+
|
|
1507
|
+
```ts nocheck
|
|
1508
|
+
// Before
|
|
1509
|
+
plugin.osd.show(`播放速度:${rate}x`);
|
|
1510
|
+
plugin.dialogs.confirm({ message: `删除 ${n} 个书签?`, confirm: "删除" });
|
|
1511
|
+
// After
|
|
1512
|
+
plugin.osd.show("Speed: {}x", [rate]);
|
|
1513
|
+
plugin.dialogs.confirm({ message: "Delete {} bookmarks?", confirm: "Delete", args: [n] });
|
|
1514
|
+
```
|
|
1515
|
+
|
|
1516
|
+
Arguments are data and are not translated. When an argument would itself be a word from a fixed set (a palette name, "on"/"off"), write one whole template per value instead, so each is translated as a sentence:
|
|
1517
|
+
|
|
1518
|
+
```ts nocheck
|
|
1519
|
+
// Before
|
|
1520
|
+
plugin.osd.show(`配色:${theme === "vestra" ? "Vestra 深色" : "Vestra 浅色"}`);
|
|
1521
|
+
// After: both branches are found by extract
|
|
1522
|
+
plugin.osd.show(theme === "vestra" ? "Palette: Vestra Dark" : "Palette: Vestra Light");
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
**A text value** (SDK 1.5.11), when the words and the data cannot be separated into a Slint file or one template: a menu item with a number, a `[string]` model, a sentence made of translated parts, a list.
|
|
1526
|
+
|
|
1527
|
+
```ts nocheck
|
|
1528
|
+
const { text, list } = plugin.i18n;
|
|
1529
|
+
// Before: the old Chinese, or English that a Chinese user then saw
|
|
1530
|
+
plugin.commands.add({ id, path: ["@audio", "Audio Track", `Track ${n} (${codec})`], run });
|
|
1531
|
+
tune.bind(() => ({ trackLabels: ["关闭字幕", ...names] }));
|
|
1532
|
+
plugin.osd.show("Download failed: {}", [english(reason)]);
|
|
1533
|
+
plugin.osd.show("Removed missing shaders: {}", [missing.join("、")]);
|
|
1534
|
+
// After
|
|
1535
|
+
plugin.commands.add({ id, path: ["@audio", "Audio Track", text("Track {} ({})", [n, codec])], run });
|
|
1536
|
+
tune.bind(() => ({ trackLabels: [text("Subtitles Off"), ...names] }));
|
|
1537
|
+
plugin.osd.show("Download failed: {}", [text(REASONS[reason])]);
|
|
1538
|
+
plugin.osd.show("Removed missing shaders: {}", [list(missing)]);
|
|
1539
|
+
```
|
|
1540
|
+
|
|
1541
|
+
The table of where text values are accepted is in Translate a plugin. A resolver's failure: `throw plugin.i18n.error("The ROM file was not found.")`. Text for the clipboard or a file: `plugin.i18n.resolve(value)`, never for the interface.
|
|
1542
|
+
|
|
1543
|
+
**Status texts as data.** A text that a window shows depending on state (`"正在载入…"`, `"没有音轨"`) becomes a state value (`loading: true`) that the Slint file turns into words.
|
|
1544
|
+
|
|
1545
|
+
Error messages that come from the player or the SDK (`error.message`) are not the plugin's text: showing them as they are is fine, but do not put them into a template of your own.
|
|
1546
|
+
|
|
1547
|
+
#### 4. The manifest
|
|
1548
|
+
|
|
1549
|
+
`name`, `punchline`, `description` and `tags` are English; their translations go in the same `.po` files (`extract` collects them). The plugin manager and the store show them translated (`PluginInfo.display`).
|
|
1550
|
+
|
|
1551
|
+
#### 5. Write `zh-Hans.po` from the old text
|
|
1552
|
+
|
|
1553
|
+
```sh
|
|
1554
|
+
vestra-plugin i18n extract --locale zh-Hans # locales/messages.pot and an empty locales/zh-Hans.po
|
|
1555
|
+
```
|
|
1556
|
+
|
|
1557
|
+
Then fill every `msgstr` with the text the plugin showed before. That text **is** the translation; copy it exactly, including punctuation and spacing, so users see no change. Placeholders stay as in the English: `msgid "Speed: {}x"` → `msgstr "播放速度:{}x"`. A plural entry in Simplified Chinese needs only `msgstr[0]`. A `git diff` of the migration shows each old text next to its new English.
|
|
1558
|
+
|
|
1559
|
+
#### 6. Check
|
|
1560
|
+
|
|
1561
|
+
```sh
|
|
1562
|
+
vestra-plugin i18n extract # again after every source change
|
|
1563
|
+
vestra-plugin i18n check # until it prints nothing but the coverage line
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
`check` finds what is left: Chinese text in TypeScript or Slint, Slint text without `@tr`, sentences composed in TypeScript, values it cannot read, translations whose placeholders differ.
|
|
1567
|
+
|
|
1568
|
+
#### 7. Tests
|
|
1569
|
+
|
|
1570
|
+
- Assertions on declarations now see English: `command.declaration.path` is what the code declared.
|
|
1571
|
+
- To check what a Chinese user sees, start with `locale: "zh-Hans"` and assert on `command.display`, `host.osd`, `host.confirmTexts`; the old expected strings usually stay as they were.
|
|
1572
|
+
- `startPlugin` without `locale` plays English for the plugin's texts but keeps the player's own messages (core actions, refusals) in Chinese, as before 1.5.9. Pass `locale` to get the player's English.
|
|
1573
|
+
|
|
1574
|
+
```ts nocheck
|
|
1575
|
+
const { host, stop } = await startPlugin(activate, { directory, locale: "zh-Hans" });
|
|
1576
|
+
assert.deepEqual(host.command("snapshot").display.path, ["视频", "截图", "保存当前画面…"]);
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
#### Before you hand it in
|
|
1580
|
+
|
|
1581
|
+
- [ ] No user-visible Chinese (or other non-English) text left in `.ts` and `.slint` files.
|
|
1582
|
+
- [ ] Every Slint text in `@tr`; no sentence composed in TypeScript.
|
|
1583
|
+
- [ ] `locales/messages.pot` up to date, `locales/zh-Hans.po` complete (`check`: `zh-Hans: N/N translated`).
|
|
1584
|
+
- [ ] Shared submenus use the agreed English keys.
|
|
1585
|
+
- [ ] No `i18n-ignore` left on text that only waited for text values (SDK 1.5.11): menu items with data, `[string]` models, nested reasons, lists.
|
|
1586
|
+
- [ ] `minPlayerVersion` is `0.4.3`; `vestra-plugin check`, `build` and `test` pass.
|
|
1587
|
+
|
|
1588
|
+
## How-to: Test a plugin
|
|
1589
|
+
|
|
1590
|
+
**Goal:** tests that run with `npm test` on any machine, without the player, and check what a user would see: the menu items, the window's values, the on-screen messages and what happens when the player refuses a call.
|
|
1591
|
+
|
|
1592
|
+
`@vestra-player/sdk/testing` runs the plugin against TestHost, a fake player in memory that speaks the real protocol, so the SDK's own logic (revisions, conditional commits, recovery after lost replies) runs as it does in the player. Only the player side is simplified: there is no decoding, no Slint rendering and no native window. A window is a record of its properties and callbacks, and a source is a set of facts the test chooses.
|
|
1593
|
+
|
|
1594
|
+
The tests live in `tests/*.test.ts` and use `node:test`; `vestra-plugin test` (the `test` script of Getting started) runs them. Import `@vestra-player/sdk/testing` only there, never in the plugin itself.
|
|
1595
|
+
|
|
1596
|
+
#### The shape of a test
|
|
1597
|
+
|
|
1598
|
+
The test of the Bookmark example: it prepares the source before the plugin starts, opens the window from its command, fires two Slint callbacks and checks the window and the playback position.
|
|
1599
|
+
|
|
1600
|
+
```ts
|
|
1601
|
+
import assert from "node:assert/strict";
|
|
1602
|
+
import test from "node:test";
|
|
1603
|
+
import { startPlugin, TestHost } from "@vestra-player/sdk/testing";
|
|
1604
|
+
import activate from "../main.ts";
|
|
1605
|
+
|
|
1606
|
+
test("mark the position, then jump back to it", async () => {
|
|
1607
|
+
const host = new TestHost();
|
|
1608
|
+
host.openElsewhere("/movie.mp4");
|
|
1609
|
+
host.setSource({ duration: 600, position: 42 });
|
|
1610
|
+
const { stop } = await startPlugin(activate, {
|
|
1611
|
+
directory: new URL("..", import.meta.url),
|
|
1612
|
+
host,
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
await host.command("bookmark.open").run();
|
|
1616
|
+
const window = host.window("bookmark");
|
|
1617
|
+
window.emit("mark");
|
|
1618
|
+
await host.settle();
|
|
1619
|
+
assert.equal(window.props.label, "Bookmark: 42.0 s");
|
|
1620
|
+
|
|
1621
|
+
host.setSource({ position: 300 }); // playback moves on
|
|
1622
|
+
window.emit("jump");
|
|
1623
|
+
await host.settle();
|
|
1624
|
+
assert.equal(host.source.position, 42);
|
|
1625
|
+
await stop();
|
|
1626
|
+
});
|
|
1627
|
+
```
|
|
1628
|
+
|
|
1629
|
+
Every test follows the same five steps:
|
|
1630
|
+
|
|
1631
|
+
1. **Start** the plugin with startPlugin(activate, { directory }). It reads `manifest.json` in `directory` (from `tests/`, `new URL("..", import.meta.url)`), so the declared capabilities apply: a call that needs an undeclared one fails with `capability`, as in the player. It resolves once activation and every registration made during it are done, and rejects when the player would fail the activation (`activate` throws, or a registration made during it fails, for example with `limit`, `conflict` or `invalid`).
|
|
1632
|
+
2. **Act** the way the user or another plugin would (below).
|
|
1633
|
+
3. **Wait** with `await host.settle()`.
|
|
1634
|
+
4. **Assert** on what the host recorded.
|
|
1635
|
+
5. **Stop** with `await stop()`: the plugin is disposed as when the player stops it (scopes released, stores flushed) and the host's temporary files are removed. Each test creates its own host.
|
|
1636
|
+
|
|
1637
|
+
`startPlugin` also returns `plugin`, the object `activate` received, to call the API directly (`plugin.player.state`). Its options: `host` (a host you prepared before activation, as above), `dataDirectory` (an existing folder as `plugin.dataDirectory`, to test a restart), `locale` (play another interface language, see Translate a plugin), and `id` with `capabilities` to start without a `manifest.json`.
|
|
1638
|
+
|
|
1639
|
+
#### Act like the user
|
|
1640
|
+
|
|
1641
|
+
| To simulate | Call |
|
|
1642
|
+
| --- | --- |
|
|
1643
|
+
| choosing a menu item | `await host.command(id).run()`; rejects like the player when the command is disabled or busy |
|
|
1644
|
+
| pressing a key | `await host.press("Ctrl+B")`; `host.press("Delete", { panel: true })` while the plugin's panel has focus |
|
|
1645
|
+
| a control in a window | `host.window(id).emit(callback, ...args)` (the id is the file name: `ui/picture.slint` is `picture`) |
|
|
1646
|
+
| closing a window with its title-bar button | `host.window(id).userClose()` (runs the window's `onClose`, unlike the plugin's own `close()`) |
|
|
1647
|
+
| a control in the mounted panel | `host.mounted().emit(callback, ...args)` |
|
|
1648
|
+
| answering a dialog | before the action: `host.dialogAnswers.push(["/media/movie.mkv"])` for a file dialog, `null` for Cancel, `{ confirmed: true, checked: false }` for `dialogs.confirm` |
|
|
1649
|
+
| another plugin calling `player.next()` | `await host.step("next")` |
|
|
1650
|
+
|
|
1651
|
+
`press` follows the player's binding table: the panel's bindings first when `panel` is true, then the global ones, the player's own commands before the plugin's. A disabled command lets the key go on to the next command bound to it (SDK 1.5.12, as player 0.4.3). `press` resolves with what the key did:
|
|
1652
|
+
|
|
1653
|
+
- `"ran"`: the first enabled command bound to the key ran;
|
|
1654
|
+
- `"reason"`: only disabled commands hold the key, and the first one with a `reason` showed it on the OSD (it is in `host.osd`) and ate the key;
|
|
1655
|
+
- `"passed"`: only disabled commands without a reason hold it; the key goes on to the focused control and nothing runs.
|
|
1656
|
+
|
|
1657
|
+
It rejects when nothing is bound to the key or the command it would run is busy. User overrides preset in `host.bindings.overlay` apply, and `host.conflicts` lists keys that several commands declare.
|
|
1658
|
+
|
|
1659
|
+
```ts
|
|
1660
|
+
import assert from "node:assert/strict";
|
|
1661
|
+
import test from "node:test";
|
|
1662
|
+
import { startPlugin } from "@vestra-player/sdk/testing";
|
|
1663
|
+
import activate from "../main.ts";
|
|
1664
|
+
|
|
1665
|
+
test("the key does nothing while no video is open", async () => {
|
|
1666
|
+
const { host, stop } = await startPlugin(activate, {
|
|
1667
|
+
directory: new URL("..", import.meta.url),
|
|
1668
|
+
});
|
|
1669
|
+
assert.equal(await host.press("Ctrl+B"), "reason");
|
|
1670
|
+
assert.deepEqual(host.osd, ["Open a video first"]);
|
|
1671
|
+
|
|
1672
|
+
host.openElsewhere("/movie.mp4");
|
|
1673
|
+
await host.settle();
|
|
1674
|
+
assert.equal(await host.press("Ctrl+B"), "ran");
|
|
1675
|
+
assert.deepEqual(host.conflicts, []);
|
|
1676
|
+
await stop();
|
|
1677
|
+
});
|
|
1678
|
+
```
|
|
1679
|
+
|
|
1680
|
+
#### Simulate the player
|
|
1681
|
+
|
|
1682
|
+
The test decides what the player is doing:
|
|
1683
|
+
|
|
1684
|
+
- `host.openElsewhere(path, phase?)`: the user or another plugin opened a source (a new source id, then `phase`, `"ready"` by default). Calls the plugin made against the old state now fail with `stale`.
|
|
1685
|
+
- `host.setSource({ phase, position, duration, paused, audioTracks, … })`: the state changed (the user paused, playback reached the end). Before `startPlugin`, it presets the source the plugin finds at activation.
|
|
1686
|
+
- `host.setMainWindow({ fullscreen, videoArea })`, `host.setAppearance(theme)`, `host.setPackages(list)`: the main window, the colour scheme, the installed plugins changed.
|
|
1687
|
+
- Preset data the player would return: `host.stats` (`player.stats()`), `host.chapters`, `host.clipboard.text`, `host.secrets`, `host.systemInfo`; `host.tools.handle("ffprobe", (args) => ({ stdout }))` makes a tool available and answers its runs.
|
|
1688
|
+
|
|
1689
|
+
#### Inject failures
|
|
1690
|
+
|
|
1691
|
+
`host.handle(method, fn)` replaces the player's answer to one protocol method until `host.reset(method)`. The handler gets the request parameters and returns the reply, or throws:
|
|
1692
|
+
|
|
1693
|
+
- `throw new HostError(code, message)`: the player refuses; the plugin's call rejects with that `code` (one of the codes in the PluginError table).
|
|
1694
|
+
- `throw new Error(…)`: the reply is lost, and the SDK runs its recovery path.
|
|
1695
|
+
|
|
1696
|
+
Method names are the protocol's (`"processing.replace"`, `"system.openPath"`, `"clipboard.writeText"`): `host.calls` lists every request in order, which shows the method an action uses, and `host.sent(method)` the parameters of one method's requests.
|
|
1697
|
+
|
|
1698
|
+
```ts
|
|
1699
|
+
import assert from "node:assert/strict";
|
|
1700
|
+
import test from "node:test";
|
|
1701
|
+
import { HostError, startPlugin } from "@vestra-player/sdk/testing";
|
|
1702
|
+
import activate from "../main.ts";
|
|
1703
|
+
|
|
1704
|
+
test("a refused change is shown and can be retried", async () => {
|
|
1705
|
+
const { host, stop } = await startPlugin(activate, {
|
|
1706
|
+
directory: new URL("..", import.meta.url),
|
|
1707
|
+
});
|
|
1708
|
+
host.openElsewhere("/movie.mp4");
|
|
1709
|
+
await host.command("picture").run();
|
|
1710
|
+
host.handle("processing.replace", () => {
|
|
1711
|
+
throw new HostError("unsupported", "no video stream");
|
|
1712
|
+
});
|
|
1713
|
+
host.window("picture").emit("adjust", "gamma", 20);
|
|
1714
|
+
await host.settle();
|
|
1715
|
+
assert.match(String(host.window("picture").props["status"]), /^Picture settings failed/);
|
|
1716
|
+
|
|
1717
|
+
host.reset("processing.replace");
|
|
1718
|
+
host.window("picture").emit("retry");
|
|
1719
|
+
await host.settle();
|
|
1720
|
+
assert.equal(host.window("picture").props["status"], "Picture settings applied");
|
|
1721
|
+
await stop();
|
|
1722
|
+
});
|
|
1723
|
+
```
|
|
1724
|
+
|
|
1725
|
+
#### Wait before asserting
|
|
1726
|
+
|
|
1727
|
+
`await host.settle(ms?)` waits until no request is in flight and the SDK's follow-up update passes ran; with `ms` it also waits that long, so that debounces and timers due by then fire (`host.settle(400)` is past `storage.json`'s 300 ms debounce). Call it after every action that makes the plugin talk to the player, before asserting. `command(id).run()` and `press` already wait.
|
|
1728
|
+
|
|
1729
|
+
`settle` waits only for calls to the player. Work the plugin does on its own (reading or parsing a file, a child process, a network request) can finish after `settle` returns on a loaded machine; poll the value you assert on instead:
|
|
1730
|
+
|
|
1731
|
+
```ts nocheck
|
|
1732
|
+
window.emit("load-file"); // the plugin reads and parses the file itself
|
|
1733
|
+
for (let i = 0; i < 100 && window.props["status-text"] !== expected; i++)
|
|
1734
|
+
await host.settle(20);
|
|
1735
|
+
assert.equal(window.props["status-text"], expected);
|
|
1736
|
+
```
|
|
1737
|
+
|
|
1738
|
+
#### What to assert on
|
|
1739
|
+
|
|
1740
|
+
- **Windows and panels:** `host.window(id).props` and `.visible`, `host.mounted().props`. Property names are the Slint (kebab-case) names: `canMark` is `can-mark`.
|
|
1741
|
+
- **Commands:** `host.command(id).state` (`enabled`, `checked`, `busy`, `reason`), `.declaration` (the English path and bindings as declared), `.display` (the menu as shown).
|
|
1742
|
+
- **Messages:** `host.osd` (every message shown, oldest first), `host.osdPanel`, `host.confirms` and `host.confirmTexts` (confirmation dialogs), `host.fileDialogs`.
|
|
1743
|
+
- **Playback and processing:** `host.source` (`position`, `paused`, `loop`, …), `host.processing`, `host.subtitles`, `host.jobs`, `host.exports`.
|
|
1744
|
+
- **Anything else the plugin sent:** `host.sent(method)`; `host.system.opened`, `host.clipboard`, `host.tools.calls` for the system services.
|
|
1745
|
+
|
|
1746
|
+
The host checks what the player checks: the command limits, the property value limits and the menu ids are enforced with the player's numbers, and processing values are validated with the player's ranges (an out-of-range `filter.set` ends in `failed` with `invalid`). Export jobs advance one step on every state query (`queued`, `running`, `completed`).
|
|
1747
|
+
|
|
1748
|
+
#### Test a restart
|
|
1749
|
+
|
|
1750
|
+
Copy `host.dataDirectory` after the first run and pass it as `dataDirectory` to the second, to check what the plugin stored and restores; the example is on startPlugin, and Remember a window's state between runs is a plugin that needs it.
|
|
1751
|
+
|
|
1752
|
+
Every member of `TestHost` is in the @vestra-player/sdk/testing reference.
|
|
1753
|
+
|
|
1754
|
+
## How-to: Publish a plugin
|
|
1755
|
+
|
|
1756
|
+
**Goal:** your plugin appears in the player's plugin store, where users install, update and roll it back.
|
|
1757
|
+
|
|
1758
|
+
The store reads the official plugin repository, [vestrasys/vestra-player-plugins](https://github.com/vestrasys/vestra-player-plugins). Every change to it is a pull request to its `main` branch, and **a merge is a release**: there are no beta channels, and CI, the user's rollback and revocation are the safety net. Every plugin in it is a third-party plugin; only the plugins built into the player are official. The repository's [README](https://github.com/vestrasys/vestra-player-plugins#contributing-a-plugin) and [CONTRIBUTING](https://github.com/vestrasys/vestra-player-plugins/blob/main/CONTRIBUTING.md) hold the full rules; this page is the author's path through them.
|
|
1759
|
+
|
|
1760
|
+
#### Two ways in
|
|
1761
|
+
|
|
1762
|
+
- **Registered** (the usual way): the source stays in your own public GitHub repository, and you register it with one file in the repository's `registry/`. CI builds the package from the commit you name, as Flathub does.
|
|
1763
|
+
- **Hosted**: the source lives in the repository's `plugins/<name>/`, and every change is a pull request there. It needs a maintainer's agreement: open an issue first.
|
|
1764
|
+
|
|
1765
|
+
> [!IMPORTANT]
|
|
1766
|
+
> Building registered plugins from source is not implemented yet. The registration format, the id rule and the owner check are final and checked by CI, but a registration pull request cannot be merged until the build lands (the repository's README shows the status).
|
|
1767
|
+
|
|
1768
|
+
#### 1. Prepare the package
|
|
1769
|
+
|
|
1770
|
+
From the plugin folder:
|
|
1771
|
+
|
|
1772
|
+
```sh
|
|
1773
|
+
npx vestra-plugin check # the manifest and a strict type check
|
|
1774
|
+
npx vestra-plugin test # tests/*.test.ts, see Test a plugin
|
|
1775
|
+
npx vestra-plugin pack # dist/ and <id>-<version>.vxp
|
|
1776
|
+
```
|
|
1777
|
+
|
|
1778
|
+
Next to `manifest.json` the store needs:
|
|
1779
|
+
|
|
1780
|
+
- the display fields in the manifest: `name`, `author`, `punchline`, `description`, `tags`, `license`, `sourceUrl`, optionally `icon`, and `minPlayerVersion` when the plugin needs a newer player than the first one (Manifest has the rules and the version table);
|
|
1781
|
+
- `README.md` (what the plugin does, how to use it, what it needs, its limits) and `CHANGELOG.md`, at most 256 KiB each: the store shows both;
|
|
1782
|
+
- tests under `tests/` that pass (Test a plugin);
|
|
1783
|
+
- only the capabilities the plugin uses: the store lists them on the plugin's page.
|
|
1784
|
+
|
|
1785
|
+
#### 2. Choose the id
|
|
1786
|
+
|
|
1787
|
+
The id is `<owner>.<name>`, where `<owner>` is the GitHub user or organization that owns the source repository (for a hosted plugin, the owner recorded in the repository's `plugins/owners.toml`), compared without case: `https://github.com/someone/cool-visualizer` publishes ids that start with `someone.`. Letters, digits, `.`, `-` and `_` only.
|
|
1788
|
+
|
|
1789
|
+
- `vestra.` is reserved for the player's built-in plugins.
|
|
1790
|
+
- An id is unique in the repository, also against a hosted plugin's and also with different case.
|
|
1791
|
+
- A changed id is a new plugin to the store: the old id's version history is not carried over.
|
|
1792
|
+
|
|
1793
|
+
#### 3. Register the plugin
|
|
1794
|
+
|
|
1795
|
+
Push the plugin to your public repository (it may live in a subdirectory) and note the full 40-character id of the commit to publish; a tag such as `v1.0.0` is good practice, but the registration pins the commit. Then fork the plugin repository and add `registry/<id>.toml`:
|
|
1796
|
+
|
|
1797
|
+
```text
|
|
1798
|
+
# registry/someone.cool-visualizer.toml
|
|
1799
|
+
repository = "https://github.com/someone/cool-visualizer"
|
|
1800
|
+
commit = "3f2a9c1d4b5e6f708192a3b4c5d6e7f8091a2b3c"
|
|
1801
|
+
path = "." # optional, default "."
|
|
1802
|
+
owners = ["someone"]
|
|
1803
|
+
```
|
|
1804
|
+
|
|
1805
|
+
| Key | Rule |
|
|
1806
|
+
| --- | --- |
|
|
1807
|
+
| file name | the plugin id plus `.toml`; equals `id` in the manifest |
|
|
1808
|
+
| `repository` | `https://github.com/<owner>/<repo>`, no trailing `/` or `.git`; `<owner>` is the id's prefix |
|
|
1809
|
+
| `commit` | the full 40-digit lowercase commit id to build |
|
|
1810
|
+
| `path` | the folder that holds `manifest.json`, relative, no `..` |
|
|
1811
|
+
| `owners` | the GitHub logins allowed to change or remove this file later; must include you |
|
|
1812
|
+
|
|
1813
|
+
No other keys. `npm ci && npm run registry` in the plugin repository checks the file.
|
|
1814
|
+
|
|
1815
|
+
A hosted plugin instead adds its folder under `plugins/` (manifest, sources, `tests/`, `README.md`, `CHANGELOG.md`) and its owner to `plugins/owners.toml`, after the maintainers agreed in the issue; `npm run plugin -- check <name>` (and `test`, `pack`) run the checks there.
|
|
1816
|
+
|
|
1817
|
+
#### 4. Open the pull request
|
|
1818
|
+
|
|
1819
|
+
Title it `Add someone.cool-visualizer` (or `Update someone.cool-visualizer to 1.2.0`) and fill in the pull request checklist. On the pull request:
|
|
1820
|
+
|
|
1821
|
+
- **Registry owners** checks that you are in `owners` of a new registration, or already an owner of the one you change. It runs the base branch's script and never runs your code.
|
|
1822
|
+
- **CI** validates the file and runs the publish flow without publishing; for a hosted plugin it also type checks, tests and packs the plugin on macOS, Windows and Linux.
|
|
1823
|
+
- **Review:** a maintainer reads the source at the registered commit (for an update, the diff since the previous one) and merges, or answers in the pull request.
|
|
1824
|
+
|
|
1825
|
+
After the merge, the repository's publish workflow builds the package, releases it as `<id>-v<version>`, signs the index and deploys it; the store shows the plugin at its next refresh.
|
|
1826
|
+
|
|
1827
|
+
#### Update, withdraw, revoke
|
|
1828
|
+
|
|
1829
|
+
- **Update:** bump `version` in the manifest, add a `CHANGELOG.md` entry, commit, and open a pull request that changes `commit` (a hosted plugin: the pull request with the change). A version is published once and never rebuilt: a change without a new version does not reach users, and a version is never reused or lowered.
|
|
1830
|
+
- **Owners:** after the first merge only an owner listed on `main` may change or remove the registration, including the list of owners; a maintainer can step in for an abandoned one.
|
|
1831
|
+
- **Withdraw:** an owner opens a pull request that deletes the registration file. The plugin drops out of the next index, so the store no longer offers it or checks it for updates; copies already installed stay installed (revocations still reach them), and the released packages stay downloadable.
|
|
1832
|
+
- **Revoke:** for a version that is broken or harmful, an owner asks a maintainer for an entry in the repository's `revoked.json`: the plugin id, the versions (`1.2.0`, or a range such as `>=1.0.0 <1.2.0`) and a reason of up to 500 characters, shown to users. Entries are never removed or edited once merged, so fix a mistake with a new version. The store disables an installed version that matches, tells the user why, and offers the fixed version when there is one; a user who enables it again is asked to confirm the risk first.
|
|
1833
|
+
|
|
1834
|
+
#### What users see in the store
|
|
1835
|
+
|
|
1836
|
+
The store shows the newest version's manifest fields, so what you write there is the plugin's page:
|
|
1837
|
+
|
|
1838
|
+
- **The list**: name and version, author and repository, the punchline. Search covers the name, id, author, punchline and tags; the list can be filtered by tag and sorted by name or by the date of the newest version. There are no download counts.
|
|
1839
|
+
- **The plugin's page**: the icon, name, author, punchline and the buttons (install, update, roll back, uninstall). Three tabs show the README, the changelog and the version history (version, release date, `minPlayerVersion`, and whether the version is current, incompatible or revoked; an older compatible version can be installed from there). A side column shows the version, the date, the license, a link to `sourceUrl` and the capabilities in plain words, the risky ones highlighted (the wording is the "Shown in the store as" column of Capabilities), with a fixed note that capabilities limit only the player API, not file or network access.
|
|
1840
|
+
- **Before installing**, the user confirms a dialog that lists the capabilities; an update that adds capabilities lists the new ones. A plugin whose every version needs a newer player shows "needs Vestra Player x.y.z or later" instead of an install button.
|
|
1841
|
+
- **The README** is rendered as Markdown; relative links and images resolve against the README's own address, and remote images are shown for the official repository.
|
|
1842
|
+
|
|
1843
|
+
## Guideline: Interface guidelines
|
|
1844
|
+
|
|
1845
|
+
These are the rules the player's built-in plugins follow for their windows, panels and messages. Follow them and your plugin looks like part of the player. They assume you know the five surfaces from Windows, panels, settings pages, dialogs and the OSD; token names, sizes, fonts, icons and widgets are listed in Theme.
|
|
1846
|
+
|
|
1847
|
+
All sizes are logical pixels (Slint `px`).
|
|
1848
|
+
|
|
1849
|
+
#### Choose the smallest surface
|
|
1850
|
+
|
|
1851
|
+
- **Feedback is an OSD message, not a window.** Confirm what just happened with osd.show ("Bookmark added", a new volume, a new speed). Do not open a dialog to say that something succeeded: the player's own snapshot command saves at once and shows a message; only "Save As…" asks.
|
|
1852
|
+
- **Errors are messages too.** A failed load or a failed action shows one sentence in the OSD; in a window the user is working in, a failure state is drawn in `Theme.danger`.
|
|
1853
|
+
- **No permanent text over the video.** Nothing sits above the video, and diagnostic numbers are not always on: show them in an OSD info block (osd.panel) behind a command with a check mark, off by default (the player's "Playback Info", Tab, works this way).
|
|
1854
|
+
- **A dialog only for a real question** (before something destructive) or a path (the system file dialogs). See Choosing a surface.
|
|
1855
|
+
|
|
1856
|
+
#### Theme
|
|
1857
|
+
|
|
1858
|
+
- **Use `Theme` tokens for every colour**, never a colour value: hard-coded colours do not follow the user's palette (`vestra` dark or `vestra-light`). `Theme.danger` is for failure states and destructive buttons, `Theme.playing` for what is playing now, `Theme.accent` for hover, "on" and selection marks, `Theme.accent-fill` for progress-like fills (sliders, levels).
|
|
1859
|
+
- **Over the video, use the OSD's colours.** The video area is dark in both palettes, so text and icons drawn on it (a backdrop, an overlay) are white (`#ffffff`) on a translucent dark plate (`#0b0a10c4`), not `Theme.text`, which is dark in `vestra-light`.
|
|
1860
|
+
- **A check mark on an accent background is white**, in both palettes.
|
|
1861
|
+
- **Do not set a font family.** Every window uses the system UI font, and Slint falls back per character (CJK included); a fixed family breaks scripts it lacks. For code or aligned numbers use `Theme.font-mono`.
|
|
1862
|
+
- **Use the four font sizes only**: `Theme.font-xs` (tiny labels), `Theme.font-body` (body text, lists, times), `Theme.font-tab` (tabs, menus), `Theme.font-title` (window and tab titles). Times and counters use tabular digits (`font-feature: tnum`) so they do not jitter.
|
|
1863
|
+
- **Do not change `Palette.color-scheme`.** The player switches the `std-widgets.slint` controls between their dark and light colours with the palette.
|
|
1864
|
+
|
|
1865
|
+
#### Icons
|
|
1866
|
+
|
|
1867
|
+
- **Icons come from `@vestra/icons.slint`.** Never use a font character as an icon (✓ › ▲ ▼ ▾ × ▶): each system's font draws it differently.
|
|
1868
|
+
- **Icon colours follow the state**: normal `Theme.text` (or `Theme.text-dim` for small icons), hover or on `Theme.accent`, pressed `Theme.accent-press`, disabled `Theme.icon-disabled`.
|
|
1869
|
+
- An icon is drawn in its own box, which is its default size; keep it, so rows and buttons line up with the player's.
|
|
1870
|
+
- For an icon of your own, draw it the same way: lines only, never filled, round caps and joins, 1.5px wide (1.25px for icons of 12px or less), kept inside the box.
|
|
1871
|
+
|
|
1872
|
+
#### Controls
|
|
1873
|
+
|
|
1874
|
+
- **Use the player's widgets** from `@vestra/widgets.slint`: `Button` (21px high; `small` 18px), `Slider` (the bottom bar style, or `panel: true` in tool windows), `ToolTipArea`, `UrlInputRow`. They take their sizes and colours from `Theme`. Other controls come from `std-widgets.slint`.
|
|
1875
|
+
- **Control sizes are tokens**: `Theme.button-height`, `Theme.button-primary-height` (the main action at the bottom of a dialog), `Theme.button-small-height` (a "Default" button in a row), `Theme.input-height`, `Theme.checkbox-row-height`, `Theme.label-height`, `Theme.form-row-gap`, `Theme.dialog-padding`.
|
|
1876
|
+
- **Corners**: `Theme.radius-control` for buttons, rows, hover plates and tooltips; `Theme.radius-panel` for menus, floating layers and larger plates.
|
|
1877
|
+
- **Icon buttons have no separators between them.** Hover and press light a rounded plate behind the icon (`Theme.hover-bg`, `Theme.press-bg`).
|
|
1878
|
+
- **Explain icon buttons with a tooltip** (`ToolTipArea`), and put long values (a full path) in a tooltip instead of a line of text.
|
|
1879
|
+
|
|
1880
|
+
#### Tool windows
|
|
1881
|
+
|
|
1882
|
+
- **No heading inside the window.** The title bar already shows the window's `title`.
|
|
1883
|
+
- **Theme sizes throughout**: buttons and inputs 21px, form rows 24–26px apart, padding 8–14px, body text `Theme.font-body`.
|
|
1884
|
+
- **Adjustments apply at once.** A window that changes picture, sound or subtitles has no "Apply" or "OK": every change takes effect while the user moves the control, and each row has its own "Default" button that resets it. Keep the slider responsive while dragging (the slider guide).
|
|
1885
|
+
- **Status and error text is one sentence**, written as English source text and translated like every other text (Translate a plugin). Never show debugging details: task ids, revisions, generations, stack traces.
|
|
1886
|
+
- **Closing a tool window does not undo its effect.** The window is a way to change a setting, not the setting's owner.
|
|
1887
|
+
|
|
1888
|
+
##### Small adjustment windows
|
|
1889
|
+
|
|
1890
|
+
The player's picture, audio and subtitle tools share one layout; a window of the same kind uses it too:
|
|
1891
|
+
|
|
1892
|
+
| Part | Size and style |
|
|
1893
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1894
|
+
| Window | 301 wide, at most 320 × 280; background `Theme.bg-bar`; content 8 from the left and right edges (285 usable) |
|
|
1895
|
+
| Tabs (if any) | 31 high, 74 wide, text `Theme.font-tab` in `Theme.text-dim`; the current tab in `Theme.text` with a short `Theme.accent-fill` line under it |
|
|
1896
|
+
| Page | background `Theme.bg-panel` |
|
|
1897
|
+
| Row | 24 high: label 65 wide, right-aligned, `Theme.font-body`; `Slider` with `panel: true`, 154 × 12; a small "Default" button, 41 × 18 |
|
|
1898
|
+
| Toggle buttons (flip, rotate) | 46 × 22; choice buttons 51 × 22 (69 wide for longer labels) |
|
|
1899
|
+
|
|
1900
|
+
##### Dialog-like windows
|
|
1901
|
+
|
|
1902
|
+
- **A link input is `UrlInputRow`**: the input with **Open** and **Cancel**, Enter accepts and Escape cancels, no history list. The player's own link windows (Open URL, the playlist's Add URL, the plugin store) all use it.
|
|
1903
|
+
- **Group the fields** of a longer form (storage, format and size, range…) with labels in a right-aligned left column; the main action is a primary button (`Theme.button-primary-height`) at the bottom.
|
|
1904
|
+
- The standard close keys close every tool window; you cannot and need not handle them.
|
|
1905
|
+
|
|
1906
|
+
#### Panels
|
|
1907
|
+
|
|
1908
|
+
- **A sidebar panel has no heading of its own.** The player shows its title in the sidebar's tab strip when there are two or more panels; with one panel there is no strip.
|
|
1909
|
+
- **No status or path lines in a panel.** Messages go to the OSD; a full path is a tooltip on its row.
|
|
1910
|
+
- **Show state with colour, not words.** As in the player's playlist: what is playing in `Theme.playing` (with a 2px bar at the left), what was played in `Theme.text-played`, the selection on `Theme.selected-bg`. A failed item gets the `failed` icon at the end of its row and an OSD message.
|
|
1911
|
+
- **Lists are plain**: single-line rows, background `Theme.bg-list`, no cards, no stripes, no rounded rows; scroll bars come from `std-widgets.slint` so they follow the palette.
|
|
1912
|
+
- **Commands of a panel go into its right-click menu**, made of the plugin's commands (panel.showMenu); see Menus.
|
|
1913
|
+
- **The bottom bar is icons only**: no text buttons, no source path, no status text (the source name is in the window title, status is an OSD message).
|
|
1914
|
+
|
|
1915
|
+
#### Text
|
|
1916
|
+
|
|
1917
|
+
- **Every visible text is English source text** that the player translates (Translate a plugin). Write short, plain sentences; one sentence per status or error.
|
|
1918
|
+
- **Leave room for longer languages.** German and French texts are usually longer than English; a Chinese, Japanese or Korean character is about one font size wide, a Latin letter about half. Let labels and buttons grow with their text (layouts, `preferred-width`; the `Button` widget does) instead of fixing widths to the English text.
|
|
1919
|
+
- **Mixed scripts must render without boxes.** That is why you use the system font (above): the player falls back per character to the system's fonts (on Windows also to a bundled CJK font) (Theme).
|
|
1920
|
+
- **Data is not translated.** File names, track names and device names show as they are.
|
|
1921
|
+
|
|
1922
|
+
#### Check your plugin
|
|
1923
|
+
|
|
1924
|
+
- Switch the palette (Settings ▸ Appearance) with your windows open: everything should follow at once.
|
|
1925
|
+
- Switch the interface language (Settings ▸ Language, used from the next start) to German and to Japanese, and look for clipped or overlapping text.
|
|
1926
|
+
- Compare your tool window with the player's Picture Adjustments window: same row heights, same buttons, same slider.
|
|
1927
|
+
|
|
1928
|
+
## Guideline: Menu guidelines
|
|
1929
|
+
|
|
1930
|
+
The player has one menu, opened with a right click on the video. There is no menu button in the window and no menu bar at the top of the screen, so every command a user may look for is in this menu or in a panel's right-click menu. It is merged from the commands of every plugin and the player's own; these rules keep the result one menu instead of a pile of plugins.
|
|
1931
|
+
|
|
1932
|
+
How paths merge, how `order` and `group` sort items and how a key finds its command are explained in Commands, menus and key bindings. Menu ids, the shared menu keys, the binding syntax and the keys the system takes first are listed in Commands. The declaration itself is CommandOptions.
|
|
1933
|
+
|
|
1934
|
+
#### Organize by what the user does
|
|
1935
|
+
|
|
1936
|
+
- **Put a command where its subject is, not under your plugin's name.** A video filter goes into Video, a subtitle tool into Subtitles, an open-something command into Open. A plugin that brings a domain of its own gets a submenu named after the domain, not after the plugin (the NES plugin's Emulator).
|
|
1937
|
+
- **Before adding an item, look for an existing one.** Every command the player and its built-in plugins add, with its menu path and default key, is listed in Built-in commands. Do not add a second entry for something the player already does, and do not scatter one feature over several menus.
|
|
1938
|
+
- **Top level is for frequent, direct commands**: opening files, closing, fullscreen and the entries that show a window or panel. Everything else goes into a submenu of its domain.
|
|
1939
|
+
- **No "File" or "View" menu.** Commands that open something go into Open ▸; commands that show a window or panel sit directly at the top level.
|
|
1940
|
+
|
|
1941
|
+
#### The top level
|
|
1942
|
+
|
|
1943
|
+
The top level as the player ships it (shortcuts in the Windows notation; the menu shows each system's own):
|
|
1944
|
+
|
|
1945
|
+
```text
|
|
1946
|
+
Open File… Ctrl+O, F3 group open
|
|
1947
|
+
Open ▸
|
|
1948
|
+
Close F4
|
|
1949
|
+
Reopen Current/Last File Ctrl+Y
|
|
1950
|
+
Copy to Clipboard ▸
|
|
1951
|
+
────────────────────────────────────────
|
|
1952
|
+
Playback ▸ group domain
|
|
1953
|
+
Subtitles ▸
|
|
1954
|
+
Video ▸
|
|
1955
|
+
Audio ▸
|
|
1956
|
+
Emulator ▸
|
|
1957
|
+
────────────────────────────────────────
|
|
1958
|
+
Toggle Fullscreen Enter group screen
|
|
1959
|
+
Screen ▸
|
|
1960
|
+
────────────────────────────────────────
|
|
1961
|
+
Playlist ✓ F6 group windows
|
|
1962
|
+
Playback Info ✓ Tab
|
|
1963
|
+
Playback Properties… Ctrl+F1
|
|
1964
|
+
Vestra Player AI…
|
|
1965
|
+
Plugin Store…
|
|
1966
|
+
────────────────────────────────────────
|
|
1967
|
+
Settings ▸
|
|
1968
|
+
────────────────────────────────────────
|
|
1969
|
+
About Vestra Player… F1
|
|
1970
|
+
────────────────────────────────────────
|
|
1971
|
+
Quit Cmd+Q group exit
|
|
1972
|
+
```
|
|
1973
|
+
|
|
1974
|
+
A top-level item uses the group and the `order` range of its kind:
|
|
1975
|
+
|
|
1976
|
+
| Group | `order` | For |
|
|
1977
|
+
| ---------------------- | ------- | -------------------------------------------------------------------------------- |
|
|
1978
|
+
| `open` | 100–199 | Opening and closing sources |
|
|
1979
|
+
| `domain` (the default) | 200–699 | Domain submenus: Playback, Subtitles, Video, Audio, and a new domain of your own |
|
|
1980
|
+
| `screen` | 700–799 | Fullscreen and the window's size and placement |
|
|
1981
|
+
| `windows` | 800–899 | Items that show a window or a panel (a check mark when it toggles one) |
|
|
1982
|
+
| `exit` | 900–999 | Quit |
|
|
1983
|
+
|
|
1984
|
+
Keep a group's orders contiguous: `group` only draws a separator between neighbours of different groups, so an item whose order falls into another group's range splits that group in two.
|
|
1985
|
+
|
|
1986
|
+
#### Inside a built-in menu
|
|
1987
|
+
|
|
1988
|
+
- **Name the menu by its id**: `["@video", "Blur"]`. The seven built-in menus (Open, Playback, Subtitles, Video, Audio, Screen, Settings) stay where they are whatever you put in them.
|
|
1989
|
+
- **Without an `order`, your item goes to the end** of the menu, after the player's own items, which is usually right for a plugin. To sit next to a related player item, give an `order` near that item's; the player's items use 100 and up, in the order listed in Built-in commands, and the ranges per menu are at CommandOptions.order.
|
|
1990
|
+
- **Group related items** with a `group` of your own, so a separator sets them apart from the player's items.
|
|
1991
|
+
- **Playback ▸ Speed and Playback ▸ Seek are the player's.** You can add to them; their titles stay the player's.
|
|
1992
|
+
|
|
1993
|
+
#### Shared submenus
|
|
1994
|
+
|
|
1995
|
+
- **Write a shared submenu exactly as the menu keys list does.** Menus merge by the English words of the path: `["@playback", "Chapters and Bookmarks", …]` joins the existing submenu, `["@playback", "Bookmarks", …]` makes a second one. The keys are in Commands.
|
|
1996
|
+
- **A new submenu that other plugins should use is a new menu key**: ask the player's maintainers to register it instead of inventing it in one plugin, so it appears in the list.
|
|
1997
|
+
- **A submenu's title is translated by the catalog of the plugin that declares its first command**; your own submenu therefore needs its words in your catalog.
|
|
1998
|
+
- **Menu paths are written with `▸`** (U+25B8) in documentation and messages, never `▶`, which many systems draw as an emoji.
|
|
1999
|
+
|
|
2000
|
+
#### Titles
|
|
2001
|
+
|
|
2002
|
+
- **Title Case**, short: "Save Current Frame", "Show Marks on Seek Bar". A menu is as wide as its longest title in the user's language; a CJK character counts about twice a Latin letter, and German or French titles are longer than English, so a long title widens the whole menu.
|
|
2003
|
+
- **End with `…`** (one character, U+2026) when the item opens a window or asks something before acting ("Adjust Sync…", "Plugin Store…"); no `…` when it acts at once.
|
|
2004
|
+
- **A toggle has a check mark** (`checked: true | false`, as "Show Subtitles"). **Mutually exclusive choices are radio items** (`radio: true`) in one group: speeds, loop modes, snapshot formats, palettes.
|
|
2005
|
+
- **Do not put key names in a title.** The menu shows each item's binding on the right, in the system's notation.
|
|
2006
|
+
- **Dynamic items are data.** Track names, device names, file names and bookmarks are shown as they are, not translated. Cap long dynamic lists well below the command limit (Limits): the player shows at most 50 chapters and ends the list with "More Chapters… (N in all)".
|
|
2007
|
+
- **Keep unavailable items, disabled, with a reason** (`enabled: false` and `reason`) instead of removing them; remove an item only when it can never apply here (the player's Picture-in-Picture item exists only where the system supports it). An empty dynamic list shows one disabled item that says so ("No Audio Tracks").
|
|
2008
|
+
|
|
2009
|
+
#### A panel's right-click menu
|
|
2010
|
+
|
|
2011
|
+
- **A panel's commands go into its own right-click menu** (panel.showMenu), not into the main menu: it looks the same and is made of the plugin's own commands. Commands that make sense only in the panel have no `path` there.
|
|
2012
|
+
- **Keys that act on the panel's content** (Enter to play, Delete to remove) are bindings with `focus: "panel"`: they win over the global meaning of the same key while the panel has focus, and give it back when it loses focus. That is intended, not a conflict.
|
|
2013
|
+
- The player's playlist is the model: its list menu, its Add and Sort buttons and its panel keys; its panel keys are at the end of Built-in commands.
|
|
2014
|
+
|
|
2015
|
+
#### Default bindings
|
|
2016
|
+
|
|
2017
|
+
A default binding is optional. Users rebind every command (Settings ▸ Keyboard Shortcuts…) and can turn all defaults off (Settings ▸ Ignore Default Shortcuts), so give one only to a command people repeat often.
|
|
2018
|
+
|
|
2019
|
+
- **Pick an unused key.** Check Built-in commands, which lists every default binding of the player and its built-in plugins. Two commands with the same key are not an error, but only one of them runs (the user's choice, then the player's own commands, then the plugin loaded first), so a colliding default simply does nothing for the user.
|
|
2020
|
+
- **Write `Ctrl`, not `Cmd`**, for a binding that works on both systems: on macOS `Ctrl+X` fires for both ⌃X and ⌘X.
|
|
2021
|
+
- **Avoid the keys the system takes first**, the standard close keys among them, as defaults.
|
|
2022
|
+
- **The right mouse button opens the menu** and is not a binding input; the other video-area mouse inputs are.
|
|
2023
|
+
- **A typed symbol is its own key**: `>` is not `Shift+.`, so `.` and `>` can do different things.
|
|
2024
|
+
- **Remember that bindings belong to the main window.** They do not fire while a tool window or the Options window has focus (Focus and keyboard).
|
|
2025
|
+
|
|
2026
|
+
## Reference: Manifest
|
|
2027
|
+
|
|
2028
|
+
`manifest.json` sits at the root of the plugin directory and of the package. The player and every `vestra-plugin` command check it against the same rules: an unknown field, an unknown capability, a field that breaks its rule, or a path that leaves the package (symbolic links included) is refused.
|
|
2029
|
+
|
|
2030
|
+
```json
|
|
2031
|
+
{
|
|
2032
|
+
"id": "me.bookmark",
|
|
2033
|
+
"version": "0.1.0",
|
|
2034
|
+
"apiVersion": "vestra/1",
|
|
2035
|
+
"entry": "main.ts",
|
|
2036
|
+
"capabilities": ["commands", "ui.window"],
|
|
2037
|
+
"name": "Bookmark",
|
|
2038
|
+
"punchline": "Mark a moment and jump back to it.",
|
|
2039
|
+
"minPlayerVersion": "0.4.3"
|
|
2040
|
+
}
|
|
2041
|
+
```
|
|
2042
|
+
|
|
2043
|
+
#### Fields
|
|
2044
|
+
|
|
2045
|
+
**Kind**: *required* fields every plugin has (`version` may come from `package.json` at build time); *display* fields are optional and shown by the plugin store and the plugin manager (player 0.2.0); *optional* changes how the player loads the plugin.
|
|
2046
|
+
|
|
2047
|
+
| Field | Kind | Rule |
|
|
2048
|
+
| --- | --- | --- |
|
|
2049
|
+
| `id` | required | 1–128 bytes of ASCII letters, digits and `.`, `-`, `_`. Ids `vestra` and `vestra.…` (any case) belong to the player's built-in plugins; installing one fails with `reserved`. A plugin in the official plugin repository is named `<GitHub user or organization>.<plugin>`; `vestra-plugin check` only hints at a missing prefix. |
|
|
2050
|
+
| `version` | required in the package | `x.y.z`, three whole numbers. `vestra-plugin build` writes it into `dist/manifest.json` from `--version`, the manifest or `package.json`. |
|
|
2051
|
+
| `apiVersion` | required | `vestra/1`. |
|
|
2052
|
+
| `entry` | required | Path of the entry module inside the package: `.ts` while developing, `.js` after `build`. |
|
|
2053
|
+
| `capabilities` | required | Capabilities from Capabilities, each at most once. |
|
|
2054
|
+
| `name` | display | 1–64 characters, one line, no leading or trailing white space. English; translated with the plugin's catalog. |
|
|
2055
|
+
| `author` | display | 1–64 characters, one line. |
|
|
2056
|
+
| `punchline` | display | One-line summary, 1–120 characters. English; translated. |
|
|
2057
|
+
| `description` | display | Plain text, 1–2000 characters; line breaks allowed. English; translated. |
|
|
2058
|
+
| `tags` | display | At most 8 tags, each 1–24 characters on one line, unique ignoring case. English; translated. |
|
|
2059
|
+
| `icon` | display | Package path of a PNG or JPEG (by content), at most 512 KiB and 1024 pixels per side; 256×256 recommended. |
|
|
2060
|
+
| `readme` | display | Package path of a UTF-8 `.md` file of at most 256 KiB, shown on the store page. Default: `README.md` when the package has one. |
|
|
2061
|
+
| `changelog` | display | The same rules; default `CHANGELOG.md` when present. |
|
|
2062
|
+
| `sourceUrl` | display | An `https:` address of at most 2048 bytes. |
|
|
2063
|
+
| `license` | display | 1–64 characters, preferably an SPDX expression such as `MIT`. |
|
|
2064
|
+
| `minPlayerVersion` | optional | `x.y.z`: the oldest player that loads the plugin (compared with `plugin.app.version`, three numbers). An older player refuses it with `versionMismatch` ("requires Vestra Player ≥ x.y.z") before it reads anything else, so a manifest with newer fields fails with that, not with an unknown field. See Player versions. |
|
|
2065
|
+
|
|
2066
|
+
- `name`, `punchline`, `description` and `tags` are English source text; the player shows them translated with the plugin's own catalog (Translate a plugin) through PluginInfo.
|
|
2067
|
+
- An `icon`, `readme` or `changelog` that is named but missing is refused. `vestra-plugin build` copies them (and a default `README.md` or `CHANGELOG.md`) into the package: the README is the plugin's page in the store, so write it for users.
|
|
2068
|
+
- A call to a member whose capability is not declared fails with code `capability` (Capabilities).
|
|
2069
|
+
|
|
2070
|
+
#### Player versions
|
|
2071
|
+
|
|
2072
|
+
The SDK's major version follows the plugin API: every 1.x release implements `apiVersion` `vestra/1`, minor releases only add to it, and a breaking change gets a new `apiVersion` and a new major version. Some additions also need a newer player. A plugin that uses one sets `minPlayerVersion` to the highest version it needs; an older player then refuses it with `versionMismatch` instead of failing at the first call.
|
|
2073
|
+
|
|
2074
|
+
The table lists the API whose documentation names a player version, newest last:
|
|
2075
|
+
|
|
2076
|
+
| Player | API that needs it or changed with it |
|
|
2077
|
+
| --- | --- |
|
|
2078
|
+
| 0.2.0 | Plugin.dataDirectory, MarkdownRef, Ui.markdown, PackageManifest, Packages.validateArchive |
|
|
2079
|
+
| 0.3.0 | CommandOptions, MENU_IDS |
|
|
2080
|
+
| 0.4.0 | ThemeId |
|
|
2081
|
+
| 0.4.1 | PluginError, FileResolverOptions, PackageCandidate.install, PluginWarning.code, Packages.validate |
|
|
2082
|
+
| 0.4.2 | Resolvers.fileTypes, Resolvers.onFileTypesChange |
|
|
2083
|
+
| 0.4.3 | CommandOptions.path, PropValue, View.set, Appearance.setLanguage, TemplateArgs, Text, ConfirmOptions.args, OsdPanel.set, OsdPanelOptions.maxColumns, Osd.show |
|
|
2084
|
+
| 0.5.1 | SlotState.reason, Panel.setInteraction, PlaybackStats.presentedFps |
|
|
2085
|
+
|
|
2086
|
+
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).
|
|
2087
|
+
|
|
2088
|
+
`@vestra-player/sdk/runtime` is the player's own entry point and is not covered by these rules. What changed in each SDK release is in the [CHANGELOG](https://github.com/vestrasys/vestra-player-sdk/blob/main/CHANGELOG.md).
|
|
2089
|
+
|
|
2090
|
+
## Reference: Capabilities
|
|
2091
|
+
|
|
2092
|
+
A plugin lists what it may do in the manifest's `capabilities` (Manifest). A call to a member whose capability is missing fails with code `capability`; nothing else is checked at run time, and capabilities limit only the player API, not the file system or the network (a plugin is a Node.js process). The store shows the capabilities to the user before installing, in the words of the "Shown in the store as" column; the highlighted ones reach outside the plugin's own features.
|
|
2093
|
+
|
|
2094
|
+
| Capability | Opens | Also needs | Shown in the store as | Highlighted |
|
|
2095
|
+
| --- | --- | --- | --- | --- |
|
|
2096
|
+
| `producer.av` | Player.openStream | `playback.control` | Provide audio and video it generates itself | |
|
|
2097
|
+
| `playback.read` | Player.state, Player.refresh, Player.stats, Player.chapters, Player.audioDevices, Timeline.marks | — | Read what is playing and the position | |
|
|
2098
|
+
| `playback.control` | Player.open, Player.setPaused, Player.seek, Player.setRate, Player.restart, Player.close, Player.scrub, Player.setOutput, Player.provideSequence, Player.next, Player.previous, Player.addAudioTrack, Player.selectAudioTrack, Player.step, Player.setStepSeek, Player.setLoop, Player.setAudioDevice, Power.preventSleep | — | Open media, control playback, keep the computer awake | |
|
|
2099
|
+
| `source.resolve` | Resolvers.add | — | Resolve online video addresses | |
|
|
2100
|
+
| `events` | Player.onChange, Subtitles.onChange, Timeline.onChange, MainWindow.onChange, Appearance.onChange, PanelSlot.onChange, Panel.onChange, BackdropSlot.onChange, Resolvers.onFileTypesChange | — | Receive playback changes as they happen | |
|
|
2101
|
+
| `commands` | Commands.add, Host.run | — | Add menu commands and shortcuts | |
|
|
2102
|
+
| `ui.window` | Windows.create | — | Open tool windows | |
|
|
2103
|
+
| `ui.mount` | Panels.mount, Panels.bottom, Panels.sidebar, Panels.backdrop, PanelSlot.onChange, Panel.onChange, BackdropSlot.onChange | — | Show panels in the bottom bar, the sidebar or behind the video | |
|
|
2104
|
+
| `ui.dialog` | Dialogs.confirm, Dialogs.openFile, Dialogs.openFiles, Dialogs.openDirectory, Dialogs.saveFile | — | Show file pickers and confirmations | |
|
|
2105
|
+
| `plugins.manage` | Packages.validate, Packages.validateArchive, Packages.rollback, Packages.setEnabled, Packages.uninstall | — | Install, uninstall and disable other plugins | yes |
|
|
2106
|
+
| `processing.video` | Plugin.video.filter, Plugin.video.view, Plugin.video.render | `playback.read` | Process the picture (filters, shaders) | |
|
|
2107
|
+
| `processing.audio` | Plugin.audio.filter | `playback.read`, `events` | Process the sound | |
|
|
2108
|
+
| `subtitles.read` | Subtitles.state, Subtitles.onChange, Subtitles.refresh | — | Read subtitles | |
|
|
2109
|
+
| `subtitles.control` | Subtitles.select, Subtitles.load, Subtitles.remove, Subtitles.setStyle | — | Load and change subtitles | |
|
|
2110
|
+
| `media.read` | MediaCapture.capture, Snapshot.export | `media.control` | Take snapshots and export clips | |
|
|
2111
|
+
| `media.control` | MediaCapture.capture, Snapshot.export | `media.read` | Take snapshots and export clips | |
|
|
2112
|
+
| `ui.osd` | Osd.show, Osd.panel | — | Show messages on the video | |
|
|
2113
|
+
| `clipboard` | Clipboard.readText, Clipboard.writeText, Clipboard.writeImage | — | Read and write the clipboard | yes |
|
|
2114
|
+
| `window.control` | MainWindow.setVideoArea, MainWindow.setAlwaysOnTop, MainWindow.setCursorAutoHide, MainWindow.setPictureInPicture | — | Adjust the main window (size, on top, picture-in-picture) | |
|
|
2115
|
+
| `bindings.manage` | Bindings.list, Bindings.set, Bindings.reset, Bindings.onChange | — | Change all shortcuts | yes |
|
|
2116
|
+
| `timeline.marks` | Timeline.layer | — | Show marks on the progress bar | |
|
|
2117
|
+
| `ui.drop` | Drops.provide, Panel.onDrop | — | Receive files dropped on the player | |
|
|
2118
|
+
| `ui.settings` | Settings.page, Settings.show | — | Add pages to the Options window | |
|
|
2119
|
+
| `ui.theme` | Appearance.set, Appearance.setLanguage | — | Change the player's palette | |
|
|
2120
|
+
| `secrets` | Secrets.get, Secrets.set, Secrets.delete | — | Store sign-in details in the system keychain | yes |
|
|
2121
|
+
|
|
2122
|
+
**Also needs** applies to every member of the row: declare those capabilities as well. A few members or options need one more, documented at the member:
|
|
2123
|
+
|
|
2124
|
+
- video.filter also needs `events`.
|
|
2125
|
+
- A `processed` snapshot or export (MediaCapture) also needs `playback.read` and `subtitles.read`.
|
|
2126
|
+
- A file resolver (Resolvers.add with file types) also needs `producer.av`.
|
|
2127
|
+
- Without `events`, Player.state is read at start and after the plugin's own operations, and `onChange` listeners do not fire for changes made elsewhere.
|
|
2128
|
+
|
|
2129
|
+
#### Without a capability
|
|
2130
|
+
|
|
2131
|
+
A member not in the table needs no capability: `plugin.tools`, `plugin.storage`, `plugin.system`, `plugin.app`, `plugin.ui` (images and Markdown), `plugin.i18n`, reading window.state, appearance.theme and packages.list. The table is `CAPABILITY_INFO`, which tests can import from `@vestra-player/sdk/testing` to check a manifest (CAPABILITY_INFO).
|
|
2132
|
+
|
|
2133
|
+
## Reference: Limits
|
|
2134
|
+
|
|
2135
|
+
The player bounds what one request may cost it. The limits are guards against runaway plugins, not sizes to aim for: a plugin that needs to come close to one usually needs a different design (send the rows near the view, not the whole list). The SDK checks the same limits before it sends a request, so a plugin's own tests hit them too (TestHost enforces them). A name in code style is a constant you can import from `@vestra-player/sdk/testing`.
|
|
2136
|
+
|
|
2137
|
+
#### Commands
|
|
2138
|
+
|
|
2139
|
+
A command added past a limit fails with code `limit` (its `ready` rejects; during activation, activation fails). Commands already registered are unaffected, and disposing one frees its slot at once.
|
|
2140
|
+
|
|
2141
|
+
| Limit | Value | Meaning |
|
|
2142
|
+
| --- | --- | --- |
|
|
2143
|
+
| `COMMAND_LIMITS.perPlugin` | 160 | Commands one plugin instance may have at the same time. |
|
|
2144
|
+
| `COMMAND_LIMITS.total` | 1024 | Commands of every plugin plus the player's own, together. |
|
|
2145
|
+
|
|
2146
|
+
#### Property values
|
|
2147
|
+
|
|
2148
|
+
One value written to a window, panel or settings page property (View.set, View.bind or an initial property). A value over a limit is refused whole with code `limit`; the property keeps what it showed.
|
|
2149
|
+
|
|
2150
|
+
| Limit | Value | Meaning |
|
|
2151
|
+
| --- | --- | --- |
|
|
2152
|
+
| `UI_VALUE_LIMITS.depth` | 8 | Nesting depth; the value itself is depth 0. |
|
|
2153
|
+
| `UI_VALUE_LIMITS.values` | 16384 | Values in all, counting every array, object, string, number, boolean and null, the value itself included; a Markdown document is one. |
|
|
2154
|
+
| `UI_VALUE_LIMITS.arrayRows` | 4096 | Rows of one array. |
|
|
2155
|
+
| `UI_VALUE_LIMITS.stringBytes` | 65536 (64 KiB) | UTF-8 bytes of one string or object field name. |
|
|
2156
|
+
| `UI_VALUE_LIMITS.textBytes` | 1048576 (1 MiB) | UTF-8 bytes of all strings and field names together, the text of the value's Markdown documents included. |
|
|
2157
|
+
|
|
2158
|
+
#### Text values
|
|
2159
|
+
|
|
2160
|
+
One Text or TextList, with everything nested in it. A text over a limit fails with code `invalid`.
|
|
2161
|
+
|
|
2162
|
+
| Limit | Value | Meaning |
|
|
2163
|
+
| --- | --- | --- |
|
|
2164
|
+
| `TEXT_LIMITS.depth` | 8 | Nesting depth of texts and lists; the value itself is depth 0. |
|
|
2165
|
+
| `TEXT_LIMITS.nodes` | 1024 | Texts, lists and arguments in all, the value itself included. |
|
|
2166
|
+
| `TEXT_LIMITS.args` | 16 | Arguments of one text. |
|
|
2167
|
+
| `TEXT_LIMITS.items` | 256 | Items of one list. |
|
|
2168
|
+
| `TEXT_LIMITS.stringBytes` | 4096 (4 KiB) | UTF-8 bytes of a source text or a string argument. |
|
|
2169
|
+
|
|
2170
|
+
#### Markdown
|
|
2171
|
+
|
|
2172
|
+
plugin.ui.markdown and its image downloads. A failed or oversized image shows its alt text; the call does not fail for it. The number of images per document and of blocks shown are on `Ui.markdown`.
|
|
2173
|
+
|
|
2174
|
+
| Limit | Value | Meaning |
|
|
2175
|
+
| --- | --- | --- |
|
|
2176
|
+
| textBytes | 262144 (256 KiB) | Text bytes. |
|
|
2177
|
+
| imageBytes | 4194304 (4 MiB) | Bytes of one downloaded image. |
|
|
2178
|
+
| imageTimeoutMs | 10000 (10 s) | Download time per image. |
|
|
2179
|
+
| redirects | 5 | Redirects followed per image. |
|
|
2180
|
+
| concurrency | 4 | Downloads at once, per plugin. |
|
|
2181
|
+
| cacheBytes | 33554432 (32 MiB) | Cache size per plugin. |
|
|
2182
|
+
| cacheMaxAgeMs | 86400000 (24 h) | An address is fetched again after this long. |
|
|
2183
|
+
|
|
2184
|
+
#### On-screen display
|
|
2185
|
+
|
|
2186
|
+
Counted on the text as shown, after translation and with its arguments filled in. A text over a limit fails with code `invalid`: osd.show rejects, OsdPanel.set reports to `Scope.onError`. Font sizes and placement are at Osd.
|
|
2187
|
+
|
|
2188
|
+
| Limit | Value | Meaning |
|
|
2189
|
+
| --- | --- | --- |
|
|
2190
|
+
| `OSD_LIMITS.messageChars` | 200 | Characters of one `osd.show` message (200; at least 1 that is not white space). Line breaks are shown as spaces. |
|
|
2191
|
+
| `OSD_LIMITS.panelLines` | 32 | Lines of the information block (`OsdPanel.set`). |
|
|
2192
|
+
| `OSD_LIMITS.panelBytes` | 4096 (4 KiB) | UTF-8 bytes of the information block. |
|
|
2193
|
+
| `OSD_LIMITS.minColumns` | 8 | The smallest `OsdPanelOptions.maxColumns`. |
|
|
2194
|
+
| `OSD_LIMITS.maxColumns` | 1000 | The largest `OsdPanelOptions.maxColumns`. |
|
|
2195
|
+
|
|
2196
|
+
#### Secrets
|
|
2197
|
+
|
|
2198
|
+
plugin.secrets. A name or value out of its rule fails with code `invalid`; a new name beyond the count fails with `limit`.
|
|
2199
|
+
|
|
2200
|
+
| Limit | Value | Meaning |
|
|
2201
|
+
| --- | --- | --- |
|
|
2202
|
+
| `SECRET_LIMITS.nameChars` | 64 | Characters of a name (64, at least 1), each one of `a-z`, `0-9`, `.`, `_` and `-`. |
|
|
2203
|
+
| `SECRET_LIMITS.valueBytes` | 1024 (1 KiB) | UTF-8 bytes of a value. |
|
|
2204
|
+
| `SECRET_LIMITS.names` | 256 | Different names one plugin may have stored at the same time. |
|
|
2205
|
+
|
|
2206
|
+
#### Messages
|
|
2207
|
+
|
|
2208
|
+
Every request a plugin sends travels as one protocol line. The plugin's side refuses a longer one with code `limit` before sending it; property values meet their own limits first.
|
|
2209
|
+
|
|
2210
|
+
| Limit | Value | Meaning |
|
|
2211
|
+
| --- | --- | --- |
|
|
2212
|
+
| `MESSAGE_LIMIT` | 1048576 (1 MiB) | The longest message a plugin may send the player, in UTF-8 bytes of one protocol line with its newline (1 MiB). The player stops a plugin that writes a longer one, so the plugin's side refuses such a request before sending it: it rejects with code `limit`. Property values meet `UI_VALUE_LIMITS` before this. |
|
|
2213
|
+
|
|
2214
|
+
#### Render options
|
|
2215
|
+
|
|
2216
|
+
RenderSettings; more shaders fail with `invalid`.
|
|
2217
|
+
|
|
2218
|
+
| Limit | Value | Meaning |
|
|
2219
|
+
| --- | --- | --- |
|
|
2220
|
+
| Entries of `RenderSettings.shaders` | 16 | mpv user shaders (`.glsl`): absolute paths of existing files, at most 16, in order. |
|
|
2221
|
+
|
|
2222
|
+
#### Limits documented at their API
|
|
2223
|
+
|
|
2224
|
+
- Filter chains: the number of nodes across all plugins, one `frameBlend`, and the range of every node's parameters, at VideoFilter; the audio delay at AudioFilter.
|
|
2225
|
+
- Generated streams: frame sizes and sample values at StreamFrame; a frame that breaks them ends the stream with `invalid`.
|
|
2226
|
+
- Manifest fields: Manifest.
|
|
2227
|
+
|
|
2228
|
+
## Reference: Commands, menus and bindings
|
|
2229
|
+
|
|
2230
|
+
The names to look up while declaring a command with plugin.commands.add. How menus merge and how a key finds its command is explained in Commands, menus and key bindings; where a command should go is in Guidelines: menus.
|
|
2231
|
+
|
|
2232
|
+
#### Menu paths
|
|
2233
|
+
|
|
2234
|
+
A command's `path` is a list of 1 to 8 segments in English source text, the last one the item itself: `["@video", "Snapshot", "Copy Snapshot"]`. This documentation writes a path with `▸`: **Video ▸ Snapshot ▸ Copy Snapshot**. The `▸` is notation only; a segment never contains it.
|
|
2235
|
+
|
|
2236
|
+
- Paths merge by these English texts (their **keys**), whatever the interface language, so commands whose paths start with the same keys share a submenu.
|
|
2237
|
+
- The first segment may name a built-in menu by id (`@video`) or by key (`Video`). Only the first segment is read as an id; an unknown id (`@view`) or an id with nothing after it fails with `invalid`.
|
|
2238
|
+
- A segment that contains data is a Text; it merges by its English rendering.
|
|
2239
|
+
|
|
2240
|
+
#### Built-in menus
|
|
2241
|
+
|
|
2242
|
+
| First segment | Or its key |
|
|
2243
|
+
| --- | --- |
|
|
2244
|
+
| `@open` | `Open` |
|
|
2245
|
+
| `@playback` | `Playback` |
|
|
2246
|
+
| `@subtitles` | `Subtitles` |
|
|
2247
|
+
| `@video` | `Video` |
|
|
2248
|
+
| `@audio` | `Audio` |
|
|
2249
|
+
| `@screen` | `Screen` |
|
|
2250
|
+
| `@settings` | `Settings` |
|
|
2251
|
+
|
|
2252
|
+
Players before 0.3.0 show `@video` as a menu of its own; players up to 0.4.2 titled these menus in Chinese, where only the id form reaches them. The fixed orders of the built-in menus and of the player's own items are on CommandOptions.order.
|
|
2253
|
+
|
|
2254
|
+
#### Submenu keys in use
|
|
2255
|
+
|
|
2256
|
+
The submenus that exist already, by key path. Write the same keys to add a command to one of them; a near-synonym makes a second submenu.
|
|
2257
|
+
|
|
2258
|
+
| Key path | Declared by |
|
|
2259
|
+
| --- | --- |
|
|
2260
|
+
| Playback ▸ Speed | the player |
|
|
2261
|
+
| Playback ▸ Seek | the player |
|
|
2262
|
+
| Open ▸ Recently Played | `vestra.history` |
|
|
2263
|
+
| Playback ▸ A-B Loop | `vestra.marks` |
|
|
2264
|
+
| Playback ▸ A-B Loop ▸ Loop Count | `vestra.marks` |
|
|
2265
|
+
| Playback ▸ Chapters and Bookmarks | `vestra.marks` |
|
|
2266
|
+
| Playback ▸ Playlist | `vestra.playlist` |
|
|
2267
|
+
| Playback ▸ Playlist ▸ Repeat | `vestra.playlist` |
|
|
2268
|
+
| Playback ▸ Playlist ▸ When Opening a File | `vestra.playlist` |
|
|
2269
|
+
| Playback ▸ Resume Playback | `vestra.history` |
|
|
2270
|
+
| Subtitles ▸ Online Subtitles | `vestra.subtitle-online` |
|
|
2271
|
+
| Subtitles ▸ Secondary Subtitle | `vestra.subtitles` |
|
|
2272
|
+
| Subtitles ▸ Secondary Subtitle ▸ Position | `vestra.subtitles` |
|
|
2273
|
+
| Subtitles ▸ Subtitle Style | `vestra.subtitles` |
|
|
2274
|
+
| Subtitles ▸ Subtitle Track | `vestra.subtitles` |
|
|
2275
|
+
| Subtitles ▸ Sync | `vestra.subtitles` |
|
|
2276
|
+
| Video ▸ Aspect | `vestra.video` |
|
|
2277
|
+
| Video ▸ Crop/Stretch | `vestra.video` |
|
|
2278
|
+
| Video ▸ Deinterlace | `vestra.video` |
|
|
2279
|
+
| Video ▸ Deinterlace ▸ Output | `vestra.video` |
|
|
2280
|
+
| Video ▸ Framing | `vestra.video` |
|
|
2281
|
+
| Video ▸ Image Processing | `vestra.video` |
|
|
2282
|
+
| Video ▸ Pixel Shaders | `vestra.video` |
|
|
2283
|
+
| Video ▸ Pixel Shaders ▸ HDR Tone Mapping | `vestra.video` |
|
|
2284
|
+
| Video ▸ Rotation | `vestra.video` |
|
|
2285
|
+
| Video ▸ Snapshot | `vestra.capture` |
|
|
2286
|
+
| Audio ▸ Audio Sync | `vestra.audio` |
|
|
2287
|
+
| Audio ▸ Audio Track | `vestra.audio` |
|
|
2288
|
+
| Audio ▸ Output Device | `vestra.audio` |
|
|
2289
|
+
| Audio ▸ Preset | `vestra.audio` |
|
|
2290
|
+
| Audio ▸ Processing | `vestra.audio` |
|
|
2291
|
+
| Screen ▸ Always on Top | `vestra.window` |
|
|
2292
|
+
| Screen ▸ When Playback Starts | `vestra.window` |
|
|
2293
|
+
| Settings ▸ Appearance | `vestra.preferences`, `vestra.visualizer` |
|
|
2294
|
+
| Settings ▸ Language | `vestra.preferences` |
|
|
2295
|
+
| Copy to Clipboard | `vestra.playback` |
|
|
2296
|
+
| Emulator | `vestra.nes` |
|
|
2297
|
+
|
|
2298
|
+
#### Built-in commands
|
|
2299
|
+
|
|
2300
|
+
Every command the player (`host`) and its built-in plugins register when the player starts with nothing open, in menu order; commands outside the main menu (a panel's shortcuts) come last. Items built from content (tracks, recent files, output devices) are not listed. The **Command** column is `<owner>/<id>`, the key under which the user's own bindings are stored. The same list is BUILTIN_COMMANDS, for tests that check your defaults against it.
|
|
2301
|
+
|
|
2302
|
+
| Menu item | Default bindings | Command |
|
|
2303
|
+
| --- | --- | --- |
|
|
2304
|
+
| Open File… | `Ctrl+O`, `F3` | `vestra.playback/open` |
|
|
2305
|
+
| Open ▸ Open URL… | `Ctrl+U` | `vestra.playback/open-url` |
|
|
2306
|
+
| Open ▸ Open Folder… | `F2` | `vestra.playlist/playlist.open-folder` |
|
|
2307
|
+
| Open ▸ Open Subtitle… | `Alt+O` | `vestra.subtitles/load` |
|
|
2308
|
+
| Open ▸ Load External Audio… | | `vestra.audio/audio.load` |
|
|
2309
|
+
| Open ▸ Recently Played ▸ (No Entries) | | `vestra.history/recent.empty` |
|
|
2310
|
+
| Open ▸ Recently Played ▸ Remove Invalid Entries | | `vestra.history/recent.prune` |
|
|
2311
|
+
| Open ▸ Recently Played ▸ Clear Entries | | `vestra.history/recent.clear` |
|
|
2312
|
+
| Open ▸ Recently Played ▸ Record Playback History | | `vestra.history/recent.enabled` |
|
|
2313
|
+
| Open ▸ Add Secondary Subtitle… | | `vestra.subtitles/load-secondary` |
|
|
2314
|
+
| Close | `F4` | `host/stop` |
|
|
2315
|
+
| Reopen Current/Last File | `Ctrl+Y` | `vestra.playback/reopen` |
|
|
2316
|
+
| Copy to Clipboard ▸ File Name | | `vestra.playback/copy-name` |
|
|
2317
|
+
| Copy to Clipboard ▸ Folder Path | | `vestra.playback/copy-folder` |
|
|
2318
|
+
| Copy to Clipboard ▸ Full Path | | `vestra.playback/copy-path` |
|
|
2319
|
+
| Playback ▸ Play/Pause | `Space` | `host/play-pause` |
|
|
2320
|
+
| Playback ▸ Previous | `PageUp`, `Mouse:X1` | `host/previous` |
|
|
2321
|
+
| Playback ▸ Next | `PageDown`, `Mouse:X2` | `host/next` |
|
|
2322
|
+
| Playback ▸ Seek ▸ Previous Frame | `D` | `host/frame-back` |
|
|
2323
|
+
| Playback ▸ Seek ▸ Next Frame | `F` | `host/frame-forward` |
|
|
2324
|
+
| Playback ▸ Seek ▸ Previous Keyframe | `Ctrl+Shift+ArrowLeft` | `vestra.playback/keyframe-back` |
|
|
2325
|
+
| Playback ▸ Seek ▸ Next Keyframe | `Ctrl+Shift+ArrowRight` | `vestra.playback/keyframe-forward` |
|
|
2326
|
+
| Playback ▸ Seek ▸ Back 5 s | `ArrowLeft` | `host/seek-backward` |
|
|
2327
|
+
| Playback ▸ Seek ▸ Forward 5 s | `ArrowRight` | `host/seek-forward` |
|
|
2328
|
+
| Playback ▸ Seek ▸ Back 30 s | `Ctrl+ArrowLeft` | `vestra.playback/backward-2` |
|
|
2329
|
+
| Playback ▸ Seek ▸ Forward 30 s | `Ctrl+ArrowRight` | `vestra.playback/forward-2` |
|
|
2330
|
+
| Playback ▸ Seek ▸ Back 60 s | `Shift+ArrowLeft` | `vestra.playback/backward-3` |
|
|
2331
|
+
| Playback ▸ Seek ▸ Forward 60 s | `Shift+ArrowRight` | `vestra.playback/forward-3` |
|
|
2332
|
+
| Playback ▸ Seek ▸ Back 300 s | `Ctrl+Alt+ArrowLeft` | `vestra.playback/backward-4` |
|
|
2333
|
+
| Playback ▸ Seek ▸ Forward 300 s | `Ctrl+Alt+ArrowRight` | `vestra.playback/forward-4` |
|
|
2334
|
+
| Playback ▸ Seek ▸ Previous Dialogue | `Home` | `vestra.subtitles/dialogue-previous` |
|
|
2335
|
+
| Playback ▸ Seek ▸ Next Dialogue | `End` | `vestra.subtitles/dialogue-next` |
|
|
2336
|
+
| Playback ▸ Seek ▸ Current Subtitle Start | `Ctrl+Home` | `vestra.subtitles/dialogue-current` |
|
|
2337
|
+
| Playback ▸ Seek ▸ Jump to Start | `Backspace` | `vestra.playback/jump-start` |
|
|
2338
|
+
| Playback ▸ Seek ▸ Seek Steps… | | `vestra.playback/time-jump` |
|
|
2339
|
+
| Playback ▸ Jump to Time… | `G` | `vestra.playback/jump` |
|
|
2340
|
+
| Playback ▸ Speed ▸ Normal | `Z` | `host/rate-normal` |
|
|
2341
|
+
| Playback ▸ Speed ▸ Slower 0.1 | `X` | `host/rate-down` |
|
|
2342
|
+
| Playback ▸ Speed ▸ Faster 0.1 | `C` | `host/rate-up` |
|
|
2343
|
+
| Playback ▸ Speed ▸ 0.5× | | `vestra.playback/rate-05` |
|
|
2344
|
+
| Playback ▸ Speed ▸ 1.5× | | `vestra.playback/rate-15` |
|
|
2345
|
+
| Playback ▸ Speed ▸ 2× | | `vestra.playback/rate-20` |
|
|
2346
|
+
| Playback ▸ Speed ▸ Custom… | | `vestra.playback/rate-custom` |
|
|
2347
|
+
| Playback ▸ Playlist ▸ Add Files… | | `vestra.playlist/playlist.add` |
|
|
2348
|
+
| Playback ▸ Playlist ▸ Add Link… | | `vestra.playlist/playlist.link` |
|
|
2349
|
+
| Playback ▸ Playlist ▸ Add Folder… | | `vestra.playlist/playlist.add-folder` |
|
|
2350
|
+
| Playback ▸ Playlist ▸ Repeat ▸ Off | | `vestra.playlist/playlist.repeat-none` |
|
|
2351
|
+
| Playback ▸ Playlist ▸ Repeat ▸ All | | `vestra.playlist/playlist.repeat-all` |
|
|
2352
|
+
| Playback ▸ Playlist ▸ Repeat ▸ One | | `vestra.playlist/playlist.repeat-one` |
|
|
2353
|
+
| Playback ▸ Playlist ▸ Shuffle | | `vestra.playlist/playlist.shuffle` |
|
|
2354
|
+
| Playback ▸ Playlist ▸ Open List File… | | `vestra.playlist/playlist.open-list` |
|
|
2355
|
+
| Playback ▸ Playlist ▸ Save List… | | `vestra.playlist/playlist.save-list` |
|
|
2356
|
+
| Playback ▸ Playlist ▸ When Opening a File ▸ Open Only That File | | `vestra.playlist/playlist.open-mode-only` |
|
|
2357
|
+
| Playback ▸ Playlist ▸ When Opening a File ▸ Add Similar Files | | `vestra.playlist/playlist.open-mode-similar` |
|
|
2358
|
+
| Playback ▸ Playlist ▸ When Opening a File ▸ Add All Files in the Folder | | `vestra.playlist/playlist.open-mode-folder` |
|
|
2359
|
+
| Playback ▸ Playlist ▸ Fast Advance to Next | | `vestra.playlist/playlist.fast-advance` |
|
|
2360
|
+
| Playback ▸ Playlist ▸ Remember List | | `vestra.playlist/playlist.remember` |
|
|
2361
|
+
| Playback ▸ Playlist ▸ Confirm Before Removing All | | `vestra.playlist/playlist.confirm-clear` |
|
|
2362
|
+
| Playback ▸ Disable Screen Saver While Playing | | `vestra.playback/prevent-sleep` |
|
|
2363
|
+
| Playback ▸ Hide Pointer While Playing | | `vestra.window/hide-cursor` |
|
|
2364
|
+
| Playback ▸ Show Time on Seek Bar Hover | | `vestra.playback/hover-time` |
|
|
2365
|
+
| Playback ▸ Show Thumbnail on Seek Bar Hover | | `vestra.playback/hover-thumbnail` |
|
|
2366
|
+
| Playback ▸ Resume Playback ▸ Don't Resume | | `vestra.history/resume.off` |
|
|
2367
|
+
| Playback ▸ Resume Playback ▸ Resume Automatically | | `vestra.history/resume.auto` |
|
|
2368
|
+
| Playback ▸ Resume Playback ▸ Ask Every Time | | `vestra.history/resume.ask` |
|
|
2369
|
+
| Playback ▸ Resume Playback ▸ Also Remember Audio Files | | `vestra.history/resume.audio` |
|
|
2370
|
+
| Playback ▸ Resume Playback ▸ Clear All Saved Positions | | `vestra.history/resume.clear` |
|
|
2371
|
+
| Playback ▸ Chapters and Bookmarks ▸ Chapter and Bookmark List… | `H` | `vestra.marks/marks.list` |
|
|
2372
|
+
| Playback ▸ Chapters and Bookmarks ▸ Add Bookmark | `P` | `vestra.marks/marks.add` |
|
|
2373
|
+
| Playback ▸ Chapters and Bookmarks ▸ Previous Bookmark or Chapter | `Shift+PageUp` | `vestra.marks/marks.previous` |
|
|
2374
|
+
| Playback ▸ Chapters and Bookmarks ▸ Next Bookmark or Chapter | `Shift+PageDown` | `vestra.marks/marks.next` |
|
|
2375
|
+
| Playback ▸ Chapters and Bookmarks ▸ Previous Chapter | `Ctrl+PageUp` | `vestra.marks/marks.previous-chapter` |
|
|
2376
|
+
| Playback ▸ Chapters and Bookmarks ▸ Next Chapter | `Ctrl+PageDown` | `vestra.marks/marks.next-chapter` |
|
|
2377
|
+
| Playback ▸ Chapters and Bookmarks ▸ Show Marks on Seek Bar | | `vestra.marks/marks.seekbar` |
|
|
2378
|
+
| Playback ▸ A-B Loop ▸ Set Start Point | `[` | `vestra.marks/ab.set-a` |
|
|
2379
|
+
| Playback ▸ A-B Loop ▸ Clear Start Point | `{` | `vestra.marks/ab.clear-a` |
|
|
2380
|
+
| Playback ▸ A-B Loop ▸ Start Point Back 0.1 s | `Ctrl+[` | `vestra.marks/ab.a-back` |
|
|
2381
|
+
| Playback ▸ A-B Loop ▸ Start Point Forward 0.1 s | `Alt+[` | `vestra.marks/ab.a-forward` |
|
|
2382
|
+
| Playback ▸ A-B Loop ▸ Set End Point | `]` | `vestra.marks/ab.set-b` |
|
|
2383
|
+
| Playback ▸ A-B Loop ▸ Clear End Point | `}` | `vestra.marks/ab.clear-b` |
|
|
2384
|
+
| Playback ▸ A-B Loop ▸ End Point Back 0.1 s | `Ctrl+]` | `vestra.marks/ab.b-back` |
|
|
2385
|
+
| Playback ▸ A-B Loop ▸ End Point Forward 0.1 s | `Alt+]` | `vestra.marks/ab.b-forward` |
|
|
2386
|
+
| Playback ▸ A-B Loop ▸ Move Range Back 0.1 s | `Ctrl+Alt+[` | `vestra.marks/ab.back` |
|
|
2387
|
+
| Playback ▸ A-B Loop ▸ Move Range Forward 0.1 s | `Ctrl+Alt+]` | `vestra.marks/ab.forward` |
|
|
2388
|
+
| Playback ▸ A-B Loop ▸ Loop Current Chapter or Bookmark Range | `Alt+\` | `vestra.marks/ab.segment` |
|
|
2389
|
+
| Playback ▸ A-B Loop ▸ A-B Loop On/Off | `\` | `vestra.marks/ab.toggle` |
|
|
2390
|
+
| Playback ▸ A-B Loop ▸ Loop Count ▸ Endless | | `vestra.marks/ab.count.endless` |
|
|
2391
|
+
| Playback ▸ A-B Loop ▸ Loop Count ▸ Once | | `vestra.marks/ab.count.1` |
|
|
2392
|
+
| Playback ▸ A-B Loop ▸ Loop Count ▸ 2 Times | | `vestra.marks/ab.count.2` |
|
|
2393
|
+
| Playback ▸ A-B Loop ▸ Loop Count ▸ 3 Times | | `vestra.marks/ab.count.3` |
|
|
2394
|
+
| Playback ▸ A-B Loop ▸ Loop Count ▸ 5 Times | | `vestra.marks/ab.count.5` |
|
|
2395
|
+
| Playback ▸ A-B Loop ▸ Loop Count ▸ 10 Times | | `vestra.marks/ab.count.10` |
|
|
2396
|
+
| Subtitles ▸ Show Subtitles | `Alt+H` | `vestra.subtitles/show` |
|
|
2397
|
+
| Subtitles ▸ Subtitle Track ▸ Off | | `vestra.subtitles/track-off` |
|
|
2398
|
+
| Subtitles ▸ Subtitle Track ▸ Cycle | `Alt+L` | `vestra.subtitles/track-cycle` |
|
|
2399
|
+
| Subtitles ▸ Secondary Subtitle ▸ Off | | `vestra.subtitles/second-off` |
|
|
2400
|
+
| Subtitles ▸ Secondary Subtitle ▸ Position ▸ Top | | `vestra.subtitles/second-position-top` |
|
|
2401
|
+
| Subtitles ▸ Secondary Subtitle ▸ Position ▸ Above Main Subtitle | | `vestra.subtitles/second-position-aboveMain` |
|
|
2402
|
+
| Subtitles ▸ Subtitle Style ▸ Bold | `Alt+B` | `vestra.subtitles/bold` |
|
|
2403
|
+
| Subtitles ▸ Subtitle Style ▸ Size + | `Alt+PageUp` | `vestra.subtitles/size-up` |
|
|
2404
|
+
| Subtitles ▸ Subtitle Style ▸ Size − | `Alt+PageDown` | `vestra.subtitles/size-down` |
|
|
2405
|
+
| Subtitles ▸ Subtitle Style ▸ Move Up | `Alt+ArrowUp` | `vestra.subtitles/move-up` |
|
|
2406
|
+
| Subtitles ▸ Subtitle Style ▸ Move Down | `Alt+ArrowDown` | `vestra.subtitles/move-down` |
|
|
2407
|
+
| Subtitles ▸ Subtitle Style ▸ Reset Position | `Alt+Home` | `vestra.subtitles/move-reset` |
|
|
2408
|
+
| Subtitles ▸ Subtitle Style ▸ Shadow | | `vestra.subtitles/shadow` |
|
|
2409
|
+
| Subtitles ▸ Subtitle Style ▸ Opaque Box | | `vestra.subtitles/box` |
|
|
2410
|
+
| Subtitles ▸ Original ASS/SSA Styles | | `vestra.subtitles/ass-original` |
|
|
2411
|
+
| Subtitles ▸ Sync ▸ Earlier 0.5 s | `.` | `vestra.subtitles/sync-earlier` |
|
|
2412
|
+
| Subtitles ▸ Sync ▸ Later 0.5 s | `,` | `vestra.subtitles/sync-later` |
|
|
2413
|
+
| Subtitles ▸ Sync ▸ Reset | `/` | `vestra.subtitles/sync-reset` |
|
|
2414
|
+
| Subtitles ▸ Sync ▸ Adjust Sync… | | `vestra.subtitles/sync-input` |
|
|
2415
|
+
| Subtitles ▸ Online Subtitles ▸ Search and Download Subtitles… | | `vestra.subtitle-online/search` |
|
|
2416
|
+
| Subtitles ▸ Online Subtitles ▸ Online Subtitle Settings… | | `vestra.subtitle-online/settings` |
|
|
2417
|
+
| Subtitles ▸ Reload Subtitles | `Ctrl+Alt+Y` | `vestra.subtitles/reload` |
|
|
2418
|
+
| Subtitles ▸ Refresh Subtitle Tracks | | `vestra.subtitles/refresh` |
|
|
2419
|
+
| Subtitles ▸ Subtitle Tuning… | | `vestra.subtitles/tune` |
|
|
2420
|
+
| Subtitles ▸ Subtitle Settings… | | `vestra.subtitles/settings` |
|
|
2421
|
+
| Video ▸ Pixel Shaders ▸ Use Shaders | `Shift+P` | `vestra.video/video.shader.use` |
|
|
2422
|
+
| Video ▸ Pixel Shaders ▸ Shader List… | | `vestra.video/video.shader.list` |
|
|
2423
|
+
| Video ▸ Pixel Shaders ▸ Open Shader Folder | | `vestra.video/video.shader.folder` |
|
|
2424
|
+
| Video ▸ Pixel Shaders ▸ Reload Shaders | | `vestra.video/video.shader.reload` |
|
|
2425
|
+
| Video ▸ Pixel Shaders ▸ HDR Tone Mapping ▸ Auto | | `vestra.video/video.hdr.auto` |
|
|
2426
|
+
| Video ▸ Pixel Shaders ▸ HDR Tone Mapping ▸ Off | | `vestra.video/video.hdr.off` |
|
|
2427
|
+
| Video ▸ Pixel Shaders ▸ HDR Tone Mapping ▸ BT.2390 | | `vestra.video/video.hdr.bt.2390` |
|
|
2428
|
+
| Video ▸ Pixel Shaders ▸ HDR Tone Mapping ▸ Hable | | `vestra.video/video.hdr.hable` |
|
|
2429
|
+
| Video ▸ Pixel Shaders ▸ HDR Tone Mapping ▸ Mobius | | `vestra.video/video.hdr.mobius` |
|
|
2430
|
+
| Video ▸ Pixel Shaders ▸ HDR Tone Mapping ▸ Reinhard | | `vestra.video/video.hdr.reinhard` |
|
|
2431
|
+
| Video ▸ Pixel Shaders ▸ HDR Tone Mapping ▸ Clip | | `vestra.video/video.hdr.clip` |
|
|
2432
|
+
| Video ▸ Pixel Shaders ▸ HDR Tone Mapping ▸ Dynamic Peak Detection | | `vestra.video/video.hdr.peak` |
|
|
2433
|
+
| Video ▸ Pixel Shaders ▸ HDR Settings… | | `vestra.video/video.hdr.settings` |
|
|
2434
|
+
| Video ▸ Deinterlace ▸ Cycle | `Ctrl+I` | `vestra.video/video.deinterlace.cycle` |
|
|
2435
|
+
| Video ▸ Deinterlace ▸ Auto | | `vestra.video/video.deinterlace.auto` |
|
|
2436
|
+
| Video ▸ Deinterlace ▸ Always | | `vestra.video/video.deinterlace.on` |
|
|
2437
|
+
| Video ▸ Deinterlace ▸ Never | | `vestra.video/video.deinterlace.off` |
|
|
2438
|
+
| Video ▸ Deinterlace ▸ Output ▸ By Frame | | `vestra.video/video.deinterlace.output.frame` |
|
|
2439
|
+
| Video ▸ Deinterlace ▸ Output ▸ By Field (Double Rate) | | `vestra.video/video.deinterlace.output.field` |
|
|
2440
|
+
| Video ▸ Aspect ▸ Keep Aspect Ratio | | `vestra.video/video.fit.keep` |
|
|
2441
|
+
| Video ▸ Aspect ▸ Ignore Aspect Ratio | | `vestra.video/video.fit.stretch` |
|
|
2442
|
+
| Video ▸ Aspect ▸ Fill Window (Crop Excess) | | `vestra.video/video.fit.fill` |
|
|
2443
|
+
| Video ▸ Aspect ▸ Cycle Aspect Ratio | `Shift+F6` | `vestra.video/video.aspect.cycle` |
|
|
2444
|
+
| Video ▸ Aspect ▸ Default (Source) | | `vestra.video/video.aspect.default` |
|
|
2445
|
+
| Video ▸ Aspect ▸ 4:3 | | `vestra.video/video.aspect.0` |
|
|
2446
|
+
| Video ▸ Aspect ▸ 16:9 | | `vestra.video/video.aspect.1` |
|
|
2447
|
+
| Video ▸ Aspect ▸ 16:10 | | `vestra.video/video.aspect.2` |
|
|
2448
|
+
| Video ▸ Aspect ▸ 1.85:1 | | `vestra.video/video.aspect.3` |
|
|
2449
|
+
| Video ▸ Aspect ▸ 2.35:1 | | `vestra.video/video.aspect.4` |
|
|
2450
|
+
| Video ▸ Aspect ▸ Custom (x:y) | | `vestra.video/video.aspect.custom` |
|
|
2451
|
+
| Video ▸ Aspect ▸ Custom Aspect Ratio… | | `vestra.video/video.aspect.edit` |
|
|
2452
|
+
| Video ▸ Crop/Stretch ▸ None | | `vestra.video/video.reframe.none` |
|
|
2453
|
+
| Video ▸ Crop/Stretch ▸ Stretch to 4:3 | | `vestra.video/video.reframe.pad.0` |
|
|
2454
|
+
| Video ▸ Crop/Stretch ▸ Stretch to 16:9 | | `vestra.video/video.reframe.pad.1` |
|
|
2455
|
+
| Video ▸ Crop/Stretch ▸ Stretch to 16:10 | | `vestra.video/video.reframe.pad.2` |
|
|
2456
|
+
| Video ▸ Crop/Stretch ▸ Stretch to 1.85:1 | | `vestra.video/video.reframe.pad.3` |
|
|
2457
|
+
| Video ▸ Crop/Stretch ▸ Stretch to 2.35:1 | | `vestra.video/video.reframe.pad.4` |
|
|
2458
|
+
| Video ▸ Crop/Stretch ▸ Crop to 4:3 | | `vestra.video/video.reframe.crop.0` |
|
|
2459
|
+
| Video ▸ Crop/Stretch ▸ Crop to 16:9 | | `vestra.video/video.reframe.crop.1` |
|
|
2460
|
+
| Video ▸ Crop/Stretch ▸ Crop to 16:10 | | `vestra.video/video.reframe.crop.2` |
|
|
2461
|
+
| Video ▸ Crop/Stretch ▸ Crop to 1.85:1 | | `vestra.video/video.reframe.crop.3` |
|
|
2462
|
+
| Video ▸ Crop/Stretch ▸ Crop to 2.35:1 | | `vestra.video/video.reframe.crop.4` |
|
|
2463
|
+
| Video ▸ Crop/Stretch ▸ Reset Position | | `vestra.video/video.reframe.reset` |
|
|
2464
|
+
| Video ▸ Crop/Stretch ▸ Move Left | | `vestra.video/video.reframe.left` |
|
|
2465
|
+
| Video ▸ Crop/Stretch ▸ Move Right | | `vestra.video/video.reframe.right` |
|
|
2466
|
+
| Video ▸ Crop/Stretch ▸ Move Up | | `vestra.video/video.reframe.up` |
|
|
2467
|
+
| Video ▸ Crop/Stretch ▸ Move Down | | `vestra.video/video.reframe.down` |
|
|
2468
|
+
| Video ▸ Framing ▸ Reset Framing | `Numpad5`, `Alt+Backspace` | `vestra.video/video.frame.reset` |
|
|
2469
|
+
| Video ▸ Framing ▸ Zoom In | `Numpad9`, `Alt+Plus` | `vestra.video/video.frame.zoom-in` |
|
|
2470
|
+
| Video ▸ Framing ▸ Zoom Out | `Numpad1`, `Alt+-` | `vestra.video/video.frame.zoom-out` |
|
|
2471
|
+
| Video ▸ Framing ▸ Pan → | `Ctrl+Numpad6`, `Alt+ArrowRight` | `vestra.video/video.frame.right` |
|
|
2472
|
+
| Video ▸ Framing ▸ Pan ← | `Ctrl+Numpad4`, `Alt+ArrowLeft` | `vestra.video/video.frame.left` |
|
|
2473
|
+
| Video ▸ Framing ▸ Pan ↑ | `Ctrl+Numpad8`, `Alt+ArrowUp` | `vestra.video/video.frame.up` |
|
|
2474
|
+
| Video ▸ Framing ▸ Pan ↓ | `Ctrl+Numpad2`, `Alt+ArrowDown` | `vestra.video/video.frame.down` |
|
|
2475
|
+
| Video ▸ Framing ▸ Center Frame | `Ctrl+Numpad5` | `vestra.video/video.frame.centre` |
|
|
2476
|
+
| Video ▸ Image Processing ▸ Flip Vertically | `Ctrl+P` | `vestra.video/video.flip.vertical` |
|
|
2477
|
+
| Video ▸ Image Processing ▸ Flip Horizontally | `Ctrl+Z` | `vestra.video/video.flip.horizontal` |
|
|
2478
|
+
| Video ▸ Image Processing ▸ Gaussian Blur | `Ctrl+B` | `vestra.video/video.gaussian` |
|
|
2479
|
+
| Video ▸ Image Processing ▸ Sharpen | `Ctrl+R` | `vestra.video/video.sharpen` |
|
|
2480
|
+
| Video ▸ Image Processing ▸ Edge Sharpen | | `vestra.video/video.edge` |
|
|
2481
|
+
| Video ▸ Image Processing ▸ Deblock | `Ctrl+H` | `vestra.video/video.deblock` |
|
|
2482
|
+
| Video ▸ Image Processing ▸ Deband | | `vestra.video/video.deband` |
|
|
2483
|
+
| Video ▸ Image Processing ▸ 3D Denoise | `Ctrl+N` | `vestra.video/video.denoise.d3` |
|
|
2484
|
+
| Video ▸ Image Processing ▸ Gradual Denoise | | `vestra.video/video.denoise.gradual` |
|
|
2485
|
+
| Video ▸ Image Processing ▸ Temporal Denoise | `Ctrl+M` | `vestra.video/video.denoise.temporal` |
|
|
2486
|
+
| Video ▸ Image Processing ▸ Frame Doubling | | `vestra.video/video.frame-blend` |
|
|
2487
|
+
| Video ▸ Image Processing ▸ Motion Interpolation | | `vestra.video/video.interpolation` |
|
|
2488
|
+
| Video ▸ Rotation ▸ Cycle | `Alt+K` | `vestra.video/video.rotate.cycle` |
|
|
2489
|
+
| Video ▸ Rotation ▸ No Rotation | | `vestra.video/video.rotate.0` |
|
|
2490
|
+
| Video ▸ Rotation ▸ 90° | | `vestra.video/video.rotate.90` |
|
|
2491
|
+
| Video ▸ Rotation ▸ 180° | | `vestra.video/video.rotate.180` |
|
|
2492
|
+
| Video ▸ Rotation ▸ 270° | | `vestra.video/video.rotate.270` |
|
|
2493
|
+
| Video ▸ Snapshot ▸ Save Current Frame | `Ctrl+E` | `vestra.capture/capture.save` |
|
|
2494
|
+
| Video ▸ Snapshot ▸ Save Current Frame As… | | `vestra.capture/capture.saveAs` |
|
|
2495
|
+
| Video ▸ Snapshot ▸ Copy Current Frame | `Ctrl+C` | `vestra.capture/capture.copy` |
|
|
2496
|
+
| Video ▸ Snapshot ▸ Save Rendered Frame | `Ctrl+Alt+E` | `vestra.capture/capture.saveScreen` |
|
|
2497
|
+
| Video ▸ Snapshot ▸ Save Rendered Frame As… | | `vestra.capture/capture.saveScreenAs` |
|
|
2498
|
+
| Video ▸ Snapshot ▸ Copy Rendered Frame | `Ctrl+Alt+C` | `vestra.capture/capture.copyScreen` |
|
|
2499
|
+
| Video ▸ Snapshot ▸ Burst… | `Ctrl+G` | `vestra.capture/capture.burst` |
|
|
2500
|
+
| Video ▸ Snapshot ▸ Export Clip… | | `vestra.capture/capture.clip` |
|
|
2501
|
+
| Video ▸ Snapshot ▸ Export Tasks… | | `vestra.capture/capture.tasks` |
|
|
2502
|
+
| Video ▸ Snapshot ▸ Capture as PNG | | `vestra.capture/capture.format.png` |
|
|
2503
|
+
| Video ▸ Snapshot ▸ Capture as JPEG | | `vestra.capture/capture.format.jpeg` |
|
|
2504
|
+
| Video ▸ Snapshot ▸ Capture as BMP | | `vestra.capture/capture.format.bmp` |
|
|
2505
|
+
| Video ▸ Snapshot ▸ Include Subtitles in Rendered Frame | | `vestra.capture/capture.screenSubtitles` |
|
|
2506
|
+
| Video ▸ Snapshot ▸ Snapshot Settings… | | `vestra.capture/capture.settings` |
|
|
2507
|
+
| Video ▸ Snapshot ▸ Snapshot Folder… | | `vestra.capture/capture.folder` |
|
|
2508
|
+
| Video ▸ Snapshot ▸ Open Snapshot Folder | | `vestra.capture/capture.openFolder` |
|
|
2509
|
+
| Video ▸ Picture Adjustments… | | `vestra.video/video.picture` |
|
|
2510
|
+
| Video ▸ Reset | `Q` | `vestra.video/video.reset` |
|
|
2511
|
+
| Video ▸ Brightness -1% | `W` | `vestra.video/video.brightness.down` |
|
|
2512
|
+
| Video ▸ Brightness +1% | `E` | `vestra.video/video.brightness.up` |
|
|
2513
|
+
| Video ▸ Contrast -1% | `R` | `vestra.video/video.contrast.down` |
|
|
2514
|
+
| Video ▸ Contrast +1% | `T` | `vestra.video/video.contrast.up` |
|
|
2515
|
+
| Video ▸ Saturation -1% | `Y` | `vestra.video/video.saturation.down` |
|
|
2516
|
+
| Video ▸ Saturation +1% | `U` | `vestra.video/video.saturation.up` |
|
|
2517
|
+
| Video ▸ Hue -1% | `I` | `vestra.video/video.hue.down` |
|
|
2518
|
+
| Video ▸ Hue +1% | `O` | `vestra.video/video.hue.up` |
|
|
2519
|
+
| Audio ▸ Volume + | `ArrowUp`, `Mouse:WheelUp` | `host/volume-up` |
|
|
2520
|
+
| Audio ▸ Volume − | `ArrowDown`, `Mouse:WheelDown` | `host/volume-down` |
|
|
2521
|
+
| Audio ▸ Mute | `M` | `host/mute` |
|
|
2522
|
+
| Audio ▸ Audio Workbench… | | `vestra.audio/audio.workbench` |
|
|
2523
|
+
| Audio ▸ Audio Track ▸ No Audio Tracks | | `vestra.audio/audio.track.none` |
|
|
2524
|
+
| Audio ▸ Audio Track ▸ Cycle Tracks | `Alt+A` | `vestra.audio/audio.track.cycle` |
|
|
2525
|
+
| Audio ▸ Audio Track ▸ Stereo | | `vestra.audio/audio.channels.stereo` |
|
|
2526
|
+
| Audio ▸ Audio Track ▸ Left Channel | | `vestra.audio/audio.channels.left` |
|
|
2527
|
+
| Audio ▸ Audio Track ▸ Right Channel | | `vestra.audio/audio.channels.right` |
|
|
2528
|
+
| Audio ▸ Audio Track ▸ Mono Mix | | `vestra.audio/audio.channels.mono` |
|
|
2529
|
+
| Audio ▸ Audio Track ▸ Swap Left and Right | | `vestra.audio/audio.channels.swap` |
|
|
2530
|
+
| Audio ▸ Output Device ▸ No Output Devices | | `vestra.audio/audio.device.none` |
|
|
2531
|
+
| Audio ▸ Equalizer | `Shift+E` | `vestra.audio/audio.equalizer` |
|
|
2532
|
+
| Audio ▸ Preset ▸ Flat | | `vestra.audio/audio.preset.0` |
|
|
2533
|
+
| Audio ▸ Preset ▸ Voice | | `vestra.audio/audio.preset.1` |
|
|
2534
|
+
| Audio ▸ Preset ▸ Music | | `vestra.audio/audio.preset.2` |
|
|
2535
|
+
| Audio ▸ Preset ▸ Cinema | | `vestra.audio/audio.preset.3` |
|
|
2536
|
+
| Audio ▸ Audio Sync ▸ 50 ms Earlier | `>` | `vestra.audio/audio.sync.earlier` |
|
|
2537
|
+
| Audio ▸ Audio Sync ▸ 50 ms Later | `<` | `vestra.audio/audio.sync.later` |
|
|
2538
|
+
| Audio ▸ Audio Sync ▸ Reset | `?` | `vestra.audio/audio.sync.reset` |
|
|
2539
|
+
| Audio ▸ Audio Sync ▸ Don't Keep | | `vestra.audio/audio.sync.keep.none` |
|
|
2540
|
+
| Audio ▸ Audio Sync ▸ Keep for This Run | | `vestra.audio/audio.sync.keep.session` |
|
|
2541
|
+
| Audio ▸ Audio Sync ▸ Always Keep | | `vestra.audio/audio.sync.keep.always` |
|
|
2542
|
+
| Audio ▸ Processing ▸ Normalize | `Shift+N` | `vestra.audio/audio.normalize` |
|
|
2543
|
+
| Audio ▸ Processing ▸ Crystalizer | `Shift+C` | `vestra.audio/audio.clarity` |
|
|
2544
|
+
| Audio ▸ Reset | | `vestra.audio/audio.reset` |
|
|
2545
|
+
| Audio ▸ Show Album Cover for Audio Only | | `vestra.visualizer/audio-cover` |
|
|
2546
|
+
| Emulator ▸ Open NES ROM… | | `vestra.nes/nes.open` |
|
|
2547
|
+
| Emulator ▸ Stop NES | | `vestra.nes/nes.stop` |
|
|
2548
|
+
| Emulator ▸ NES Tools… | | `vestra.nes/nes.window` |
|
|
2549
|
+
| Toggle Fullscreen | `Enter`, `Alt+Enter`, `F11`, `Mouse:LeftDoubleClick` | `host/fullscreen` |
|
|
2550
|
+
| Screen ▸ Picture-in-Picture | | `vestra.window/pip` |
|
|
2551
|
+
| Screen ▸ Mini Size | ``` | `vestra.window/mini` |
|
|
2552
|
+
| Screen ▸ 0.5× | `1` | `vestra.window/size.0.5` |
|
|
2553
|
+
| Screen ▸ 1× | `2` | `vestra.window/size.1` |
|
|
2554
|
+
| Screen ▸ 1.5× | `3` | `vestra.window/size.1.5` |
|
|
2555
|
+
| Screen ▸ 2× | `4` | `vestra.window/size.2` |
|
|
2556
|
+
| Screen ▸ Fit to Video Aspect Ratio | `8` | `vestra.window/fit` |
|
|
2557
|
+
| Screen ▸ When Playback Starts ▸ Don't Resize | | `vestra.window/auto.off` |
|
|
2558
|
+
| Screen ▸ When Playback Starts ▸ 0.5× | | `vestra.window/auto.0.5` |
|
|
2559
|
+
| Screen ▸ When Playback Starts ▸ 1× | | `vestra.window/auto.1` |
|
|
2560
|
+
| Screen ▸ When Playback Starts ▸ 1.5× | | `vestra.window/auto.1.5` |
|
|
2561
|
+
| Screen ▸ When Playback Starts ▸ 2× | | `vestra.window/auto.2` |
|
|
2562
|
+
| Screen ▸ When Playback Starts ▸ Fit to Video Aspect Ratio | | `vestra.window/auto.fit` |
|
|
2563
|
+
| Screen ▸ When Playback Starts ▸ Only Once | | `vestra.window/auto.once` |
|
|
2564
|
+
| Screen ▸ Always on Top ▸ Cycle Mode | `Ctrl+T` | `vestra.window/top.cycle` |
|
|
2565
|
+
| Screen ▸ Always on Top ▸ Never | | `vestra.window/top.never` |
|
|
2566
|
+
| Screen ▸ Always on Top ▸ Always | | `vestra.window/top.always` |
|
|
2567
|
+
| Screen ▸ Always on Top ▸ While Playing | | `vestra.window/top.playing` |
|
|
2568
|
+
| Screen ▸ Always on Top ▸ While Playing in Fullscreen | | `vestra.window/top.fullscreen` |
|
|
2569
|
+
| Playlist | `F6` | `vestra.playlist/playlist.visibility` |
|
|
2570
|
+
| Playback Info | `Tab` | `vestra.info/info.toggle` |
|
|
2571
|
+
| Playback Properties… | `Ctrl+F1` | `vestra.info/info.properties` |
|
|
2572
|
+
| Vestra Player AI… | | `vestra.workshop/workshop.open` |
|
|
2573
|
+
| Plugin Store… | | `vestra.store/open` |
|
|
2574
|
+
| Settings ▸ Options… | `F5` | `vestra.preferences/options` |
|
|
2575
|
+
| Settings ▸ Keyboard Shortcuts… | | `vestra.hotkeys/keys` |
|
|
2576
|
+
| Settings ▸ Mouse… | | `vestra.hotkeys/mouse` |
|
|
2577
|
+
| Settings ▸ Ignore Default Shortcuts | | `vestra.hotkeys/ignore-defaults` |
|
|
2578
|
+
| Settings ▸ Plugin Manager… | | `vestra.plugins/manage` |
|
|
2579
|
+
| Settings ▸ Appearance ▸ Vestra Dark | | `vestra.preferences/theme.vestra` |
|
|
2580
|
+
| Settings ▸ Appearance ▸ Vestra Light | | `vestra.preferences/theme.vestra-light` |
|
|
2581
|
+
| Settings ▸ Appearance ▸ Show Logo When Idle | | `vestra.visualizer/idle-logo` |
|
|
2582
|
+
| Settings ▸ Appearance ▸ Choose Logo Image… | | `vestra.visualizer/choose-logo` |
|
|
2583
|
+
| Settings ▸ Appearance ▸ Restore Default Logo | | `vestra.visualizer/reset-logo` |
|
|
2584
|
+
| Settings ▸ Language ▸ Follow System | | `vestra.preferences/language.system` |
|
|
2585
|
+
| Settings ▸ Language ▸ English | | `vestra.preferences/language.en` |
|
|
2586
|
+
| Settings ▸ Language ▸ 简体中文 | | `vestra.preferences/language.zh-Hans` |
|
|
2587
|
+
| Settings ▸ Language ▸ 日本語 | | `vestra.preferences/language.ja` |
|
|
2588
|
+
| Settings ▸ Language ▸ 한국어 | | `vestra.preferences/language.ko` |
|
|
2589
|
+
| Settings ▸ Language ▸ Deutsch | | `vestra.preferences/language.de` |
|
|
2590
|
+
| Settings ▸ Language ▸ Français | | `vestra.preferences/language.fr` |
|
|
2591
|
+
| Settings ▸ Language ▸ Español | | `vestra.preferences/language.es` |
|
|
2592
|
+
| About Vestra Player… | `F1` | `vestra.info/info.about` |
|
|
2593
|
+
| Quit | `Cmd+Q` | `host/quit` |
|
|
2594
|
+
| Leave Fullscreen (not in the menu) | `Escape` | `host/exit-fullscreen` |
|
|
2595
|
+
| Brief Info (not in the menu) | `Shift+Tab` | `vestra.info/info.brief` |
|
|
2596
|
+
| Play (not in the menu) | `Enter`, `P` (panel focus) | `vestra.playlist/playlist.play` |
|
|
2597
|
+
| Add Files… (not in the menu) | `Ctrl+I` (panel focus) | `vestra.playlist/playlist.list-add` |
|
|
2598
|
+
| Add Folder… (not in the menu) | `Ctrl+O` (panel focus) | `vestra.playlist/playlist.list-add-folder` |
|
|
2599
|
+
| Add Link… (not in the menu) | `Ctrl+U` (panel focus) | `vestra.playlist/playlist.list-link` |
|
|
2600
|
+
| Remove Selected (not in the menu) | `Delete`, `Backspace` (panel focus) | `vestra.playlist/playlist.remove` |
|
|
2601
|
+
| Remove Unselected (not in the menu) | `Ctrl+Delete` (panel focus) | `vestra.playlist/playlist.remove-unselected` |
|
|
2602
|
+
| Remove Invalid Items (not in the menu) | `Ctrl+T` (panel focus) | `vestra.playlist/playlist.remove-invalid` |
|
|
2603
|
+
| Remove All (not in the menu) | `Alt+Delete` (panel focus) | `vestra.playlist/playlist.clear` |
|
|
2604
|
+
| Show in Finder (not in the menu) | `Ctrl+F` (panel focus) | `vestra.playlist/playlist.reveal` |
|
|
2605
|
+
| Move to Top (not in the menu) | `Ctrl+Home` (panel focus) | `vestra.playlist/playlist.move-top` |
|
|
2606
|
+
| Move Up (not in the menu) | `Ctrl+ArrowUp` (panel focus) | `vestra.playlist/playlist.move-up` |
|
|
2607
|
+
| Move Down (not in the menu) | `Ctrl+ArrowDown` (panel focus) | `vestra.playlist/playlist.move-down` |
|
|
2608
|
+
| Move to Bottom (not in the menu) | `Ctrl+End` (panel focus) | `vestra.playlist/playlist.move-bottom` |
|
|
2609
|
+
| Ascending (not in the menu) | `Ctrl+1` (panel focus) | `vestra.playlist/playlist.sort-ascending` |
|
|
2610
|
+
| Descending (not in the menu) | `Ctrl+2` (panel focus) | `vestra.playlist/playlist.sort-descending` |
|
|
2611
|
+
| By Title (not in the menu) | `Ctrl+3` (panel focus) | `vestra.playlist/playlist.sort-title` |
|
|
2612
|
+
| By File Name (not in the menu) | `Ctrl+4` (panel focus) | `vestra.playlist/playlist.sort-filename` |
|
|
2613
|
+
| By Extension (not in the menu) | `Ctrl+5` (panel focus) | `vestra.playlist/playlist.sort-extension` |
|
|
2614
|
+
| By Size (not in the menu) | `Ctrl+6` (panel focus) | `vestra.playlist/playlist.sort-size` |
|
|
2615
|
+
| By Duration (not in the menu) | `Ctrl+7` (panel focus) | `vestra.playlist/playlist.sort-duration` |
|
|
2616
|
+
| By Date (not in the menu) | `Ctrl+8` (panel focus) | `vestra.playlist/playlist.sort-date` |
|
|
2617
|
+
| Random Order (not in the menu) | `Ctrl+9` (panel focus) | `vestra.playlist/playlist.sort-shuffle` |
|
|
2618
|
+
| By Folder (not in the menu) | `Ctrl+0` (panel focus) | `vestra.playlist/playlist.sort-folder` |
|
|
2619
|
+
| Sort by Path (not in the menu) | | `vestra.playlist/playlist.sort-path` |
|
|
2620
|
+
| Reverse Current Order (not in the menu) | | `vestra.playlist/playlist.sort-reverse` |
|
|
2621
|
+
| Select All (not in the menu) | `Ctrl+A` (panel focus) | `vestra.playlist/playlist.select-all` |
|
|
2622
|
+
| Search (not in the menu) | `Ctrl+S` (panel focus) | `vestra.playlist/playlist.search` |
|
|
2623
|
+
| Open List File… (not in the menu) | `F3` (panel focus) | `vestra.playlist/playlist.list-open-list` |
|
|
2624
|
+
| Save List… (not in the menu) | `F2` (panel focus) | `vestra.playlist/playlist.list-save-list` |
|
|
2625
|
+
| Close List (not in the menu) | `F6` (panel focus) | `vestra.playlist/playlist.close` |
|
|
2626
|
+
| Playlist Settings… (not in the menu) | `F5` (panel focus) | `vestra.playlist/playlist.settings` |
|
|
2627
|
+
|
|
2628
|
+
#### Binding syntax
|
|
2629
|
+
|
|
2630
|
+
```text
|
|
2631
|
+
binding = { modifier "+" } input modifiers in the order Ctrl, Alt, Shift, Cmd, each at most once
|
|
2632
|
+
input = key name | one printable symbol | mouse input
|
|
2633
|
+
```
|
|
2634
|
+
|
|
2635
|
+
- A letter is its key name in capitals, whatever the case typed: `Shift+E`.
|
|
2636
|
+
- A symbol is written as typed and never with `Shift`: `">"`, not `"Shift+."`; `"!"`, not `"Shift+1"`. The `+` key is always `Plus`, also with modifiers (`Alt+Plus`).
|
|
2637
|
+
- `"5"` is the main keyboard's 5 only, `"Numpad5"` the keypad's only; `"Enter"` is not the keypad's Enter (`"NumpadEnter"`). The menu shows keypad names as `Num5`, `Num+`, `NumEnter`.
|
|
2638
|
+
- `Delete` is the forward-delete key ⌦ (fn+⌫ on a Mac laptop), `Backspace` is ⌫.
|
|
2639
|
+
- On macOS `Ctrl` also matches ⌘ and `Cmd` only ⌘; on Windows `Cmd` is the Windows logo key. Write `Ctrl+…` for a binding that works on both.
|
|
2640
|
+
- Mouse inputs apply over the video area only. The right button is not bindable (it opens the context menu).
|
|
2641
|
+
- Anything else fails with code `invalid` when the command is added.
|
|
749
2642
|
|
|
750
|
-
|
|
2643
|
+
A menu item shows the first of its keyboard bindings that is in effect.
|
|
751
2644
|
|
|
752
|
-
|
|
753
|
-
| ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
754
|
-
| VideoFilter VideoFilter`.set`, AudioFilter AudioFilter`.set` | Player.seek player.seek: use Player.scrub player.scrub() and `Scrub.seek` for a seek slider |
|
|
755
|
-
| Player.setOutput player.setOutput (volume, mute) | `player.setRate` and other player controls |
|
|
756
|
-
| `Scrub.seek` (about one seek per display frame) | Osd.show osd.show: show a message on release |
|
|
757
|
-
| View properties (`bind`, `set`), OsdPanel.set OsdPanel.set | `JsonStore.save` is debounced (300 ms), so it is fine; anything you write yourself is not |
|
|
2645
|
+
#### Binding names
|
|
758
2646
|
|
|
759
|
-
|
|
2647
|
+
| Field of `BINDING_INPUTS` | Names | Notes |
|
|
2648
|
+
| --- | --- | --- |
|
|
2649
|
+
| `modifiers` | `Ctrl`, `Alt`, `Shift`, `Cmd` | Modifiers, in the order a binding writes them. `Cmd` is ⌘ on macOS and the Windows logo key on Windows; on macOS `Ctrl` also matches ⌘. |
|
|
2650
|
+
| `letters` | `A`–`Z` | Letter keys, by key name whatever the case: `A` to `Z`. |
|
|
2651
|
+
| `digits` | `0`–`9` | Digit keys of the main keyboard: `0` to `9`. |
|
|
2652
|
+
| `functionKeys` | `F1`–`F24` | Function keys: `F1` to `F24`. |
|
|
2653
|
+
| `named` | `Space`, `Enter`, `Tab`, `Escape`, `Backspace`, `Delete`, `Insert`, `Home`, `End`, `PageUp`, `PageDown`, `ArrowLeft`, `ArrowRight`, `ArrowUp`, `ArrowDown`, `Plus`, `Numpad0`, `Numpad1`, `Numpad2`, `Numpad3`, `Numpad4`, `Numpad5`, `Numpad6`, `Numpad7`, `Numpad8`, `Numpad9`, `NumpadPlus`, `NumpadMinus`, `NumpadMultiply`, `NumpadDivide`, `NumpadEnter`, `NumpadDecimal` | Other named keys, matched by the physical key. `Plus` is the `+` key however it is typed; the `Numpad…` names are the numeric keypad only, never the main keyboard. |
|
|
2654
|
+
| `mouse` | `Mouse:LeftClick`, `Mouse:LeftDoubleClick`, `Mouse:LeftDoubleClick:LeftHalf`, `Mouse:LeftDoubleClick:RightHalf`, `Mouse:MiddleClick`, `Mouse:X1`, `Mouse:X2`, `Mouse:WheelUp`, `Mouse:WheelDown`, `Mouse:WheelLeft`, `Mouse:WheelRight` | Mouse inputs on the video area. `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a double click. |
|
|
760
2655
|
|
|
761
|
-
|
|
2656
|
+
#### Core actions
|
|
762
2657
|
|
|
763
|
-
|
|
764
|
-
import type { Plugin } from "@vestra-player/sdk";
|
|
2658
|
+
The player's own playback commands, which plugin.host.run runs by id (capability `commands`). Their bindings are stored as `host/<action>`; what each one does is on HostAction.
|
|
765
2659
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
2660
|
+
| Action | Menu item | Default bindings |
|
|
2661
|
+
| --- | --- | --- |
|
|
2662
|
+
| `stop` | Close | `F4` |
|
|
2663
|
+
| `play-pause` | Playback ▸ Play/Pause | `Space` |
|
|
2664
|
+
| `previous` | Playback ▸ Previous | `PageUp`, `Mouse:X1` |
|
|
2665
|
+
| `next` | Playback ▸ Next | `PageDown`, `Mouse:X2` |
|
|
2666
|
+
| `frame-back` | Playback ▸ Seek ▸ Previous Frame | `D` |
|
|
2667
|
+
| `frame-forward` | Playback ▸ Seek ▸ Next Frame | `F` |
|
|
2668
|
+
| `seek-backward` | Playback ▸ Seek ▸ Back 5 s | `ArrowLeft` |
|
|
2669
|
+
| `seek-forward` | Playback ▸ Seek ▸ Forward 5 s | `ArrowRight` |
|
|
2670
|
+
| `rate-normal` | Playback ▸ Speed ▸ Normal | `Z` |
|
|
2671
|
+
| `rate-down` | Playback ▸ Speed ▸ Slower 0.1 | `X` |
|
|
2672
|
+
| `rate-up` | Playback ▸ Speed ▸ Faster 0.1 | `C` |
|
|
2673
|
+
| `volume-up` | Audio ▸ Volume + | `ArrowUp`, `Mouse:WheelUp` |
|
|
2674
|
+
| `volume-down` | Audio ▸ Volume − | `ArrowDown`, `Mouse:WheelDown` |
|
|
2675
|
+
| `mute` | Audio ▸ Mute | `M` |
|
|
2676
|
+
|
|
2677
|
+
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.
|
|
2678
|
+
|
|
2679
|
+
#### Keys the system takes first
|
|
2680
|
+
|
|
2681
|
+
These keys may never reach a binding, because the system or the player takes them first; do not use them as defaults. This table is the one list of them; other pages link here.
|
|
2682
|
+
|
|
2683
|
+
| Keys | Taken by |
|
|
2684
|
+
| --- | --- |
|
|
2685
|
+
| ⌃F1–⌃F8 (macOS) | The system's keyboard navigation (⌃F1 turns keyboard access on and off). A `Ctrl+F1` binding still fires with ⌘F1. |
|
|
2686
|
+
| ⌃← ⌃→ ⌃↑ ⌃↓ (macOS) | Spaces, Mission Control and application windows. The ⌘ versions of a `Ctrl+arrow` binding work. |
|
|
2687
|
+
| ⌘H, ⌘M (macOS) | The system: hide and minimize the application. A `Ctrl+H` or `Ctrl+M` binding then fires only with ⌃. |
|
|
2688
|
+
| ⌘Tab, ⌘Space (macOS) | The application switcher and Spotlight. |
|
|
2689
|
+
| F11, and F1–F12 on Apple keyboards (macOS) | F11 shows the desktop; on Apple keyboards the function keys are media keys unless fn is held. |
|
|
2690
|
+
| Ctrl+Alt+arrow keys (Windows) | Some graphics drivers, to rotate the screen. |
|
|
2691
|
+
| Most `Cmd+…` combinations (Windows) | The system, for the Windows logo key. `Cmd+Q` (**Quit**) does nothing there; the window's close button and Alt+F4 quit. |
|
|
2692
|
+
| **Standard close keys**: ⌘W (macOS); Ctrl+W, Ctrl+F4 (Windows) | The player, in a tool window, the floating sidebar and the Options window: they close that window exactly like the title-bar button (a tool window's `onClose` runs), even while a text field has focus, so neither Slint handlers nor bindings see them. In the main window they do nothing, so a binding for them fires there as usual. |
|
|
2693
|
+
| Right button, left drag on the video | The player: the context menu; moving the window in window mode. |
|
|
785
2694
|
|
|
786
|
-
|
|
2695
|
+
The defaults of the built-in commands are taken as well: a plugin default on the same input in the same scope loses to the player's own command and to plugins loaded earlier.
|
|
787
2696
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
2697
|
+
## Reference: Platforms
|
|
2698
|
+
|
|
2699
|
+
A plugin uses the same API on macOS and Windows; the player does the system-specific part. This table lists everything that behaves differently. A call that a system cannot serve fails with `unsupported`, so a plugin handles that code instead of testing for the system (plugin.app.system() tells the system when a plugin must know).
|
|
2700
|
+
|
|
2701
|
+
| Feature | macOS | Windows |
|
|
2702
|
+
| --- | --- | --- |
|
|
2703
|
+
| Clipboard | Text; PNG and JPEG images as their original bytes. | Text; images are converted to a bitmap and PNG. |
|
|
2704
|
+
| Power.preventSleep | `caffeinate`. | The thread execution state. |
|
|
2705
|
+
| Dialogs.confirm | An `NSAlert` sheet on the active player window (usually the tool window that asked), with the custom buttons and the checkbox. | A task dialog owned by the active player window, titled "Vestra Player", with the custom buttons; `destructive` shows a warning icon; a `checkbox` is asked as a second yes/no dialog after the user confirms. |
|
|
2706
|
+
| MainWindow.setAlwaysOnTop | The floating window level. | Topmost window. |
|
|
2707
|
+
| MainWindow.setVideoArea | The visible screen area, without the menu bar and the Dock. | The whole display. |
|
|
2708
|
+
| MainWindow.setPictureInPicture | The system's picture-in-picture (macOS 14 or newer). | `unsupported`. |
|
|
2709
|
+
| DropModifiers | The pointer and ⌥ ⇧ ⌘ (`cmd` is ⌘). | The pointer and Alt, Shift, Ctrl (`cmd` is Ctrl). |
|
|
2710
|
+
| plugin.system (open, reveal) | Opened by the system; revealing several files selects all of them in one Finder window. | Opened by the shell; one Explorer window per folder. |
|
|
2711
|
+
| plugin.secrets | The login keychain. | The Credential Manager. |
|
|
2712
|
+
| App.system() | Version, build, model and displays. | Version, build, model and displays. |
|
|
2713
|
+
| Snapshots and exports (MediaCapture) | Available. | Available; needs FFmpeg 6.1 or newer. |
|
|
2714
|
+
| Generated streams (Player.openStream) | A Unix socket. | Loopback TCP with a token. |
|
|
2715
|
+
| Rendering | On the GPU (OpenGL), with hardware decoding. | In software, with hardware decoding copied back (`d3d11va-copy` and similar). |
|
|
2716
|
+
| Render options (RenderOptions) | Available; `peakDetect` has no effect. | `unsupported` (software rendering). |
|
|
2717
|
+
| Bindings | `Ctrl` also matches ⌘; `Cmd` is ⌘. | `Cmd` is the Windows logo key; `Cmd+Q` does not quit. |
|
|
2718
|
+
| Keys taken first, standard close keys | See Keys the system takes first. | See Keys the system takes first. |
|
|
2719
|
+
| Interface font | The system font; CJK falls back per character to the system's fonts (PingFang, Hiragino). | The system font; CJK falls back per character to Windows' CJK fonts in the interface language's order, then the bundled Noto Sans CJK SC (the chain). |
|
|
2720
|
+
| `Theme.font-mono` | Menlo | Consolas |
|
|
2721
|
+
|
|
2722
|
+
Other systems have no released player; there, every feature in this table that needs the system fails with `unsupported` (always on top and `setVideoArea` with `unavailable`, `preventSleep` has no effect), and keys behave as on Windows.
|
|
2723
|
+
|
|
2724
|
+
When the player runs without a user interface (headless, as in some automated setups), opening windows, mounting panels, dialogs, the OSD and `settings.show` fail with `unavailable` on every system.
|
|
2725
|
+
|
|
2726
|
+
## Reference: Theme
|
|
2727
|
+
|
|
2728
|
+
What a plugin's Slint file can import from the player's library. How Slint and TypeScript talk is in Windows, panels, settings pages, dialogs and the OSD; which token to use where is in Guidelines: interface.
|
|
2729
|
+
|
|
2730
|
+
```slint nocheck
|
|
2731
|
+
import { Theme } from "@vestra/theme.slint";
|
|
2732
|
+
import { Icon } from "@vestra/icons.slint";
|
|
2733
|
+
import { Button, Slider, ToolTipArea, UrlInputRow } from "@vestra/widgets.slint";
|
|
2734
|
+
import { MarkdownView, MarkdownBlocks, MarkdownBlock } from "@vestra/markdown.slint";
|
|
798
2735
|
```
|
|
799
2736
|
|
|
800
|
-
|
|
2737
|
+
> **Current values; they may change — use the tokens.** The values below are what the player uses today. A later player may adjust them; a token keeps its meaning, a value you copy does not follow.
|
|
801
2738
|
|
|
802
|
-
|
|
2739
|
+
#### Colours
|
|
803
2740
|
|
|
804
|
-
|
|
2741
|
+
`Theme` colours follow the user's palette: `vestra`, the dark default, and `vestra-light` (player 0.4.0), whose video area stays dark. The player sets the palette on every view and again when the user switches, so read colours from `Theme` (they are read-only `out` properties) and never write a colour value yourself. The table shows both palettes' current values; the same values are THEME_PALETTES, for tests.
|
|
805
2742
|
|
|
806
|
-
|
|
2743
|
+
| Token | Value in `vestra` | Value in `vestra-light` | Note |
|
|
2744
|
+
| --- | --- | --- | --- |
|
|
2745
|
+
| `Theme.bg-bar` | `#16141c` | `#f7f6fa` | |
|
|
2746
|
+
| `Theme.bg-video` | `#0a090e` | `#0e0d12` | |
|
|
2747
|
+
| `Theme.bg-list` | `#1a1821` | `#ffffff` | |
|
|
2748
|
+
| `Theme.bg-panel` | `#201d28` | `#f1eff5` | |
|
|
2749
|
+
| `Theme.line` | `#2b2734` | `#dcd8e4` | |
|
|
2750
|
+
| `Theme.hover-bg` | `#28232f` | `#e9e6f0` | |
|
|
2751
|
+
| `Theme.press-bg` | `#110f16` | `#dcd8e6` | |
|
|
2752
|
+
| `Theme.selected-bg` | `#2f2840` | `#efe4fb` | |
|
|
2753
|
+
| `Theme.text` | `#f5f3fa` | `#1c1924` | |
|
|
2754
|
+
| `Theme.text-dim` | `#8e88a0` | `#6f6a7f` | |
|
|
2755
|
+
| `Theme.text-list` | `#bdb8cb` | `#3b3748` | |
|
|
2756
|
+
| `Theme.text-played` | `#6e6980` | `#9a95a8` | |
|
|
2757
|
+
| `Theme.accent` | `#f72aa6` | `#d4128a` | |
|
|
2758
|
+
| `Theme.accent-press` | `#c41a83` | `#a80d6c` | |
|
|
2759
|
+
| `Theme.accent-disabled` | `#5a1d48` | `#ecbcd9` | |
|
|
2760
|
+
| `Theme.icon-disabled` | `#3b3646` | `#c9c5d2` | |
|
|
2761
|
+
| `Theme.playing` | `#2bb6ff` | `#0773ba` | |
|
|
2762
|
+
| `Theme.button-bg` | `#2f2a3b` | `#e6e3ec` | |
|
|
2763
|
+
| `Theme.button-hover` | `#3a3448` | `#dcd8e4` | |
|
|
2764
|
+
| `Theme.button-press` | `#25202e` | `#cfcadb` | |
|
|
2765
|
+
| `Theme.button-disabled` | `#1b1922` | `#efedf3` | |
|
|
2766
|
+
| `Theme.danger` | `#ff5a5f` | `#c42b2b` | Errors and destructive actions: a failed status line, a Delete button. |
|
|
2767
|
+
| `Theme.accent-fill` | gradient 90°: `#fcc402 0%`, `#fb5a35 20%`, `#fa0aa8 45%`, `#7a0afa 72%`, `#10a8fc 100%` | gradient 90°: `#f5a700 0%`, `#f0442f 20%`, `#e0089a 45%`, `#6a08e8 72%`, `#0a95e6 100%` | Brand gradient for "progress" fills (seek bar, volume, sliders, accents). |
|
|
2768
|
+
|
|
2769
|
+
`std-widgets.slint` controls (`CheckBox`, `ComboBox`, `LineEdit`…) are compiled in Slint's `fluent-dark` style and switched to their light colours in `vestra-light`; do not set `Palette.color-scheme` yourself.
|
|
2770
|
+
|
|
2771
|
+
#### Sizes and fonts
|
|
2772
|
+
|
|
2773
|
+
Corner radii, font sizes and control sizes are constants, the same in both palettes. `radius-control` rounds controls, menu rows and hover plates; `radius-panel` menus, panels and the OSD (4px and 8px since player 0.4.0; 3px and 6px before).
|
|
2774
|
+
|
|
2775
|
+
| Token | Value | Note |
|
|
2776
|
+
| --- | --- | --- |
|
|
2777
|
+
| `Theme.radius-control` | `4px` | |
|
|
2778
|
+
| `Theme.radius-panel` | `8px` | |
|
|
2779
|
+
| `Theme.font-xs` | `9px` | |
|
|
2780
|
+
| `Theme.font-body` | `11px` | |
|
|
2781
|
+
| `Theme.font-tab` | `12px` | |
|
|
2782
|
+
| `Theme.font-title` | `13px` | |
|
|
2783
|
+
| `Theme.button-height` | `21px` | |
|
|
2784
|
+
| `Theme.button-min-width` | `66px` | |
|
|
2785
|
+
| `Theme.button-padding` | `8px` | |
|
|
2786
|
+
| `Theme.button-primary-height` | `24px` | |
|
|
2787
|
+
| `Theme.button-small-height` | `18px` | |
|
|
2788
|
+
| `Theme.button-small-min-width` | `41px` | |
|
|
2789
|
+
| `Theme.input-height` | `21px` | |
|
|
2790
|
+
| `Theme.checkbox-row-height` | `18px` | |
|
|
2791
|
+
| `Theme.label-height` | `14px` | |
|
|
2792
|
+
| `Theme.form-row-gap` | `26px` | |
|
|
2793
|
+
| `Theme.dialog-padding` | `14px` | |
|
|
2794
|
+
|
|
2795
|
+
- **No font family of its own.** Every window uses the system's interface font for Latin text; a plugin does not need to set a font, and a `default-font-family` it sets stays first. A character the font lacks falls back, character by character, to a font for its script.
|
|
2796
|
+
- **CJK fallback.** On macOS the system's own fallback (PingFang, Hiragino) covers Chinese, Japanese and Korean. On Windows the player sets a chain per script for every window, plugin windows included, and uses the first installed family that has the character:
|
|
2797
|
+
1. Windows' CJK families in the interface language's order: for Han text, the language's own tradition first (Simplified Chinese, the default for other languages: Microsoft YaHei UI, Microsoft YaHei, DengXian, SimSun, NSimSun, SimHei; Traditional Chinese: Microsoft JhengHei UI, Microsoft JhengHei, PMingLiU, MingLiU; Japanese: Yu Gothic UI, Meiryo UI, Yu Gothic, Meiryo, MS UI Gothic, MS Gothic; Korean: Malgun Gothic, Gulim, Dotum, Batang), then the other traditions. Kana starts with the Japanese families, Hangul with the Korean, Bopomofo with the Traditional Chinese.
|
|
2798
|
+
2. The family Windows' own fallback picks.
|
|
2799
|
+
3. The bundled Noto Sans CJK SC, last, so CJK text never shows as boxes when an edition lacks the optional font packs.
|
|
2800
|
+
- **`Theme.font-mono`** is the system's monospace family: Menlo on macOS, Consolas on Windows (Platforms). CJK falls back in it too.
|
|
2801
|
+
|
|
2802
|
+
#### Icons
|
|
2803
|
+
|
|
2804
|
+
`@vestra/icons.slint` is the player's own icon set (player 0.4.0): round-capped, round-joined lines, 1.5px wide (1.25px for the small glyphs), never filled, each in its own box.
|
|
2805
|
+
|
|
2806
|
+
```slint nocheck
|
|
2807
|
+
Icon { name: "play"; color: Theme.text; } // its box size
|
|
2808
|
+
Icon { name: "close"; width: 20px; height: 20px; } // scaled
|
|
2809
|
+
```
|
|
2810
|
+
|
|
2811
|
+
- The box is the icon's default size; set `width` and `height` to scale it. An unknown name draws nothing.
|
|
2812
|
+
- For a `Path` of your own, `Icons.commands(name)` returns the path data, `Icons.box-width(name)` and `Icons.box-height(name)` the box (the path's viewbox) and `Icons.line(name)` the line width.
|
|
2813
|
+
|
|
2814
|
+
| Name | Box (px) | Line |
|
|
2815
|
+
| --- | --- | --- |
|
|
2816
|
+
| `play` | 12 × 14 | 1.5px |
|
|
2817
|
+
| `pause` | 12 × 14 | 1.5px |
|
|
2818
|
+
| `stop` | 12 × 12 | 1.5px |
|
|
2819
|
+
| `back` | 14 × 10 | 1.5px |
|
|
2820
|
+
| `forward` | 14 × 10 | 1.5px |
|
|
2821
|
+
| `open` | 14 × 12 | 1.5px |
|
|
2822
|
+
| `volume` | 13 × 10 | 1.5px |
|
|
2823
|
+
| `mute` | 13 × 10 | 1.5px |
|
|
2824
|
+
| `playlist` | 13 × 10 | 1.5px |
|
|
2825
|
+
| `detach` | 14 × 12 | 1.5px |
|
|
2826
|
+
| `attach` | 14 × 12 | 1.5px |
|
|
2827
|
+
| `close` | 14 × 13 | 1.5px |
|
|
2828
|
+
| `failed` | 10 × 10 | 1.25px |
|
|
2829
|
+
| `move-top` | 14 × 12 | 1.5px |
|
|
2830
|
+
| `move-up` | 14 × 12 | 1.5px |
|
|
2831
|
+
| `move-down` | 14 × 12 | 1.5px |
|
|
2832
|
+
| `move-bottom` | 14 × 12 | 1.5px |
|
|
2833
|
+
| `search` | 14 × 12 | 1.5px |
|
|
2834
|
+
| `chapter` | 12 × 12 | 1.25px |
|
|
2835
|
+
| `bookmark` | 12 × 12 | 1.25px |
|
|
2836
|
+
| `check-task` | 11 × 11 | 1.5px |
|
|
2837
|
+
| `check-box` | 9 × 9 | 1.5px |
|
|
2838
|
+
| `menu-check` | 14 × 14 | 1.5px |
|
|
2839
|
+
| `submenu` | 5 × 8 | 1.25px |
|
|
2840
|
+
| `scroll-up` | 8 × 5 | 1.25px |
|
|
2841
|
+
| `scroll-down` | 8 × 5 | 1.25px |
|
|
2842
|
+
| `caret-down` | 8 × 5 | 1.25px |
|
|
2843
|
+
| `sort-asc` | 7 × 4 | 1.1px |
|
|
2844
|
+
| `sort-desc` | 7 × 4 | 1.1px |
|
|
2845
|
+
| `tag-remove` | 7 × 7 | 1.25px |
|
|
2846
|
+
| `preview-play` | 8 × 9 | 1.25px |
|
|
2847
|
+
| `add` | 10 × 10 | 1.5px |
|
|
2848
|
+
| `remove` | 10 × 10 | 1.5px |
|
|
2849
|
+
| `sort` | 10 × 10 | 1.5px |
|
|
2850
|
+
|
|
2851
|
+
#### Widgets
|
|
2852
|
+
|
|
2853
|
+
`@vestra/widgets.slint`: compact controls drawn with the theme. Other controls come from `std-widgets.slint`.
|
|
2854
|
+
|
|
2855
|
+
**`ToolTipArea`**: Hover for 600 ms to show `text` beside the pointer. The tip closes when the pointer leaves or presses; it shows again on the next hover.
|
|
2856
|
+
|
|
2857
|
+
| Member | Kind | Default | Note |
|
|
2858
|
+
| --- | --- | --- | --- |
|
|
2859
|
+
| `text` | in `string` | | |
|
|
2860
|
+
| `showing` | out `bool` | | The tip is showing (read-only). |
|
|
807
2861
|
|
|
808
|
-
|
|
2862
|
+
**`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.
|
|
809
2863
|
|
|
810
|
-
|
|
2864
|
+
| Member | Kind | Default | Note |
|
|
2865
|
+
| --- | --- | --- | --- |
|
|
2866
|
+
| `text` | in `string` | | |
|
|
2867
|
+
| `icon` | in `string` | | |
|
|
2868
|
+
| `icon-width` | in `length` | `14px` | |
|
|
2869
|
+
| `icon-height` | in `length` | `12px` | |
|
|
2870
|
+
| `stroked` | in `bool` | | |
|
|
2871
|
+
| `enabled` | in `bool` | `true` | |
|
|
2872
|
+
| `small` | in `bool` | | |
|
|
2873
|
+
| `checked` | in `bool` | | Icon buttons: the "on" state, drawn in the accent colour. |
|
|
2874
|
+
| `clicked` | callback `()` | | |
|
|
2875
|
+
|
|
2876
|
+
**`Slider`**: 2px line slider. Bar style (default): 8px round thumb, a white core in a 2px accent ring, gradient fill. Panel style (`panel: true`): 14×12 thumb, gradient fill. The track is `Theme.line` in both.
|
|
2877
|
+
|
|
2878
|
+
| Member | Kind | Default | Note |
|
|
2879
|
+
| --- | --- | --- | --- |
|
|
2880
|
+
| `value` | in-out `float` | | |
|
|
2881
|
+
| `minimum` | in `float` | `0` | |
|
|
2882
|
+
| `maximum` | in `float` | `1` | |
|
|
2883
|
+
| `enabled` | in `bool` | `true` | |
|
|
2884
|
+
| `panel` | in `bool` | | |
|
|
2885
|
+
| `changed` | callback `(float)` | | |
|
|
2886
|
+
| `released` | callback `(float)` | | |
|
|
811
2887
|
|
|
812
|
-
|
|
813
|
-
{
|
|
814
|
-
"id": "examples.brightness",
|
|
815
|
-
"version": "1.0.0",
|
|
816
|
-
"apiVersion": "vestra/1",
|
|
817
|
-
"entry": "main.ts",
|
|
818
|
-
"minPlayerVersion": "0.3.0",
|
|
819
|
-
"capabilities": [
|
|
820
|
-
"commands",
|
|
821
|
-
"ui.window",
|
|
822
|
-
"processing.video",
|
|
823
|
-
"playback.read",
|
|
824
|
-
"events"
|
|
825
|
-
]
|
|
826
|
-
}
|
|
827
|
-
```
|
|
2888
|
+
**`UrlInputRow`**: Link input: a multi-line input and [Open] [Cancel]. Enter or the confirm button accepts (Shift+Enter starts a new line), Esc or Cancel cancels. No validation here: the plugin checks the address.
|
|
828
2889
|
|
|
829
|
-
|
|
2890
|
+
| Member | Kind | Default | Note |
|
|
2891
|
+
| --- | --- | --- | --- |
|
|
2892
|
+
| `text` | in-out `string` | | |
|
|
2893
|
+
| `placeholder` | in `string` | | |
|
|
2894
|
+
| `confirm-text` | in `string` | `@tr("Open")` | |
|
|
2895
|
+
| `cancel-text` | in `string` | `@tr("Cancel")` | |
|
|
2896
|
+
| `enabled` | in `bool` | `true` | |
|
|
2897
|
+
| `accepted` | callback `(string)` | | |
|
|
2898
|
+
| `cancelled` | callback `()` | | |
|
|
2899
|
+
| `focus-input` | function `()` | | |
|
|
2900
|
+
| `select-all` | function `()` | | |
|
|
830
2901
|
|
|
831
|
-
`
|
|
2902
|
+
A component's public functions are called from your own Slint code, through an element id: `row.focus-input()` puts the keyboard focus into `UrlInputRow`'s input (call it when the window opens, for example from `init`), `row.select-all()` selects its text.
|
|
832
2903
|
|
|
833
|
-
|
|
834
|
-
import type { Plugin } from "@vestra-player/sdk";
|
|
2904
|
+
#### Markdown
|
|
835
2905
|
|
|
836
|
-
|
|
837
|
-
interface Saved {
|
|
838
|
-
/** −1–1, 0 = unchanged. */
|
|
839
|
-
brightness: number;
|
|
840
|
-
/** The window was open when the player quit. */
|
|
841
|
-
open: boolean;
|
|
842
|
-
}
|
|
2906
|
+
`@vestra/markdown.slint` shows a document made by plugin.ui.markdown: give `blocks` its value. Links report `link-clicked(url)`, resolved against the document's base; the plugin decides what to open (usually plugin.system). `code-copy: true` adds a Copy button to code blocks that reports `copy-code(code)`; the plugin copies it (plugin.clipboard).
|
|
843
2907
|
|
|
844
|
-
|
|
2908
|
+
**`MarkdownBlocks`**: Markdown blocks without scrolling, as tall as their content.
|
|
845
2909
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
typeof v.brightness === "number" &&
|
|
853
|
-
Math.abs(v.brightness) <= 1 &&
|
|
854
|
-
typeof v.open === "boolean"
|
|
855
|
-
);
|
|
856
|
-
}
|
|
2910
|
+
| Member | Kind | Default | Note |
|
|
2911
|
+
| --- | --- | --- | --- |
|
|
2912
|
+
| `blocks` | in `[MarkdownBlock]` | | |
|
|
2913
|
+
| `code-copy` | in `bool` | `false` | A Copy button on code blocks, reporting `copy-code(code)`. |
|
|
2914
|
+
| `link-clicked` | callback `(string)` | | |
|
|
2915
|
+
| `copy-code` | callback `(string)` | | |
|
|
857
2916
|
|
|
858
|
-
|
|
859
|
-
// 1. Load: read once, synchronously; defaults if there is no valid file.
|
|
860
|
-
const store = plugin.storage.json<Saved>("brightness", {
|
|
861
|
-
defaults: DEFAULTS,
|
|
862
|
-
validate: isSaved,
|
|
863
|
-
});
|
|
864
|
-
/** Change part of the state; the file is written 300 ms after the last change. */
|
|
865
|
-
const remember = (patch: Partial<Saved>) =>
|
|
866
|
-
store.save({ ...store.value, ...patch });
|
|
2917
|
+
**`MarkdownView`**: A scrolling Markdown document.
|
|
867
2918
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
saturation: 1,
|
|
876
|
-
gamma: 1,
|
|
877
|
-
},
|
|
878
|
-
]);
|
|
879
|
-
apply(); // the effect is back before the window is
|
|
2919
|
+
| Member | Kind | Default | Note |
|
|
2920
|
+
| --- | --- | --- | --- |
|
|
2921
|
+
| `blocks` | in `[MarkdownBlock]` | | |
|
|
2922
|
+
| `inset` | in `length` | `10px` | Space around the document. |
|
|
2923
|
+
| `code-copy` | in `bool` | `false` | A Copy button on code blocks, reporting `copy-code(code)`. |
|
|
2924
|
+
| `link-clicked` | callback `(string)` | | |
|
|
2925
|
+
| `copy-code` | callback `(string)` | | |
|
|
880
2926
|
|
|
881
|
-
|
|
882
|
-
const window = plugin.windows.create("ui/brightness.slint");
|
|
883
|
-
window.bind(() => ({
|
|
884
|
-
brightness: store.value.brightness,
|
|
885
|
-
label: store.value.brightness.toFixed(2),
|
|
886
|
-
}));
|
|
887
|
-
window.on("brightnessChanged", (value: number) => {
|
|
888
|
-
remember({ brightness: Math.min(1, Math.max(-1, value)) });
|
|
889
|
-
apply();
|
|
890
|
-
});
|
|
891
|
-
window.on("reset", () => {
|
|
892
|
-
remember({ brightness: 0 });
|
|
893
|
-
apply();
|
|
894
|
-
});
|
|
2927
|
+
Struct **`MarkdownRow`**: One table row (cells are plain text).
|
|
895
2928
|
|
|
896
|
-
|
|
897
|
-
window.onClose(() => remember({ open: false }));
|
|
898
|
-
const open = async () => {
|
|
899
|
-
await window.show();
|
|
900
|
-
remember({ open: true });
|
|
901
|
-
};
|
|
902
|
-
plugin.commands.add({
|
|
903
|
-
id: "brightness",
|
|
904
|
-
path: ["@video", "Brightness…"],
|
|
905
|
-
run: open,
|
|
906
|
-
});
|
|
2929
|
+
Struct **`MarkdownBlock`**: One displayed block. Plugins only pass these through; the fields are filled by the player.
|
|
907
2930
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
2931
|
+
## Reference: Command line
|
|
2932
|
+
|
|
2933
|
+
`vestra-plugin` comes with `@vestra-player/sdk` and works in any project that depends on it (`npx vestra-plugin …`, or an npm script). A plugin directory is a directory with `manifest.json`; the default is the current directory, and several can be given at once.
|
|
2934
|
+
|
|
2935
|
+
```text
|
|
2936
|
+
usage: vestra-plugin <check|build|test|pack> [plugin-dir...] [--version x.y.z] [--out dir]
|
|
2937
|
+
vestra-plugin i18n extract [plugin-dir...] [--locale <lang>]...
|
|
2938
|
+
vestra-plugin i18n check [plugin-dir...]
|
|
911
2939
|
```
|
|
912
2940
|
|
|
913
|
-
|
|
2941
|
+
| Command | Does |
|
|
2942
|
+
| --- | --- |
|
|
2943
|
+
| `vestra-plugin check` | strict type check of the plugin's .ts files and tests/*.ts |
|
|
2944
|
+
| `vestra-plugin build` | check, then write dist/: .ts → .js with types stripped, other package files copied, dist/manifest.json with `version` |
|
|
2945
|
+
| `vestra-plugin test` | node --test on tests/*.test.{ts,mjs} |
|
|
2946
|
+
| `vestra-plugin pack` | build, then dist/ → <out>/<id>-<version>.vxp (a zip; default out: the current directory) with its sha256 and size |
|
|
2947
|
+
| `vestra-plugin i18n extract` | find the texts to translate (@tr in .slint files, the texts handed to commands.add, osd.show, dialogs, settings and panel titles, the manifest's display fields), write locales/messages.pot and merge it into every locales/<lang>.po (translations kept, vanished ones marked obsolete); --locale <lang> starts a new language |
|
|
2948
|
+
| `vestra-plugin i18n check` | coverage per language, placeholder and plural problems, a stale messages.pot, and texts that cannot be translated (composed in TypeScript, not literals, not English, Slint text without @tr) as warnings; fails (exit 1) on a stale messages.pot, a .po missing messages, an unreadable file |
|
|
914
2949
|
|
|
915
|
-
|
|
916
|
-
- **Bind the stored value, not a copy.** `window.bind` reads `store.value` in every update pass, so whatever changes the state (a callback, Reset, a command) updates the window, and a window opened later starts with the current values.
|
|
917
|
-
- **Save on every change; the store debounces.** `save()` replaces `store.value` at once and writes the file 300 ms after the last call (a temporary file, then a rename). Dragging the slider writes the file once, after the drag. When the plugin stops (the player quits, the user disables the plugin), the store is flushed, so the last value is not lost.
|
|
918
|
-
- **"Open" means "the user did not close it".** `onClose` runs only when the user closes the window (title-bar button, ⌘W, Ctrl+W, Ctrl+F4). When the player quits, the plugin stops and its windows close _without_ `onClose`, so `open` stays `true` and the window comes back next time. That is the behaviour users expect from an application's tool windows.
|
|
919
|
-
- **Restore after binding.** The window is created and bound before `open()` runs, so it opens with the stored values and without flashing defaults. If it cannot open (for example in a player without a user interface), the plugin forgets that it was open instead of failing activation.
|
|
2950
|
+
Exit codes: 0 on success, 1 when a command failed for any of the directories (each one is still tried), 2 for an unknown command.
|
|
920
2951
|
|
|
921
|
-
|
|
2952
|
+
#### Rules every command applies
|
|
922
2953
|
|
|
923
|
-
- **
|
|
924
|
-
- **
|
|
2954
|
+
- **The manifest first.** Every command checks `manifest.json` against the player's own rules (Manifest) before anything else. `check` also prints hints that do not fail, such as an id without the `<owner>.` prefix the official repository needs.
|
|
2955
|
+
- **Type checking** extends the nearest `tsconfig.json` at or above the plugin directory (up to the repository root), else built-in strict settings (`strict`, `NodeNext`, `verbatimModuleSyntax`, `erasableSyntaxOnly`). Only the plugin's own files and `tests/` are checked, with the SDK's own TypeScript 5.x.
|
|
2956
|
+
- **`import type` only.** A plugin never imports SDK code at run time; `build` fails when the output still refers to `@vestra-player/sdk`. Tests import `@vestra-player/sdk/testing` freely.
|
|
2957
|
+
- **Translations.** With a `locales/` folder, `check` prints the `i18n check` problems as warnings and `build` compiles each `locales/<lang>.po` to `dist/locales/<lang>.json` (Translate a plugin).
|
|
925
2958
|
|
|
926
|
-
|
|
2959
|
+
#### What build puts into dist/
|
|
927
2960
|
|
|
928
|
-
-
|
|
929
|
-
-
|
|
930
|
-
-
|
|
2961
|
+
- `.ts` files with the types stripped and relative imports changed to `.js`; `dist/manifest.json` with `entry` changed to `.js` and `version` written.
|
|
2962
|
+
- `.slint` and `.mjs` files, data files and `licenses/` as they are; the manifest's `icon`, `readme` and `changelog`, and `README.md` and `CHANGELOG.md` when present.
|
|
2963
|
+
- Left out: `tests/`, `node_modules/`, `vendor/`, other `*.md`, `tsconfig*.json`, `package*.json`, `.po` and `.pot` files.
|
|
2964
|
+
- The version is `--version`, else the manifest's `version`, else `package.json`'s. `--version` takes one plugin directory.
|
|
2965
|
+
- `dist/` is build output; do not commit it.
|
|
2966
|
+
|
|
2967
|
+
`pack` runs `build` and writes `dist/` as `<id>-<version>.vxp` (a zip) to `--out` (default: the current directory), printing its size and SHA-256. The same input gives the same bytes (sorted entries, fixed timestamps), so a package can be rebuilt and compared.
|
|
2968
|
+
|
|
2969
|
+
#### The vestra field of package.json
|
|
2970
|
+
|
|
2971
|
+
A plugin with npm dependencies says how to package them in its own `package.json`:
|
|
2972
|
+
|
|
2973
|
+
```json
|
|
2974
|
+
{
|
|
2975
|
+
"vestra": {
|
|
2976
|
+
"bundle": { "external": ["some-native-module"] },
|
|
2977
|
+
"assets": ["vendor"]
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
```
|
|
2981
|
+
|
|
2982
|
+
- `"bundle": true` or `{ "external": [...] }` bundles the entry and its dependencies into one file with the plugin's own `esbuild` (a devDependency). **A plugin with runtime dependencies must bundle**: the package has no `node_modules`.
|
|
2983
|
+
- `"assets": [...]` copies extra files or directories into the package as they are.
|
|
2984
|
+
|
|
2985
|
+
To try a plugin without packing it, start the player with `--plugin <folder>`; it runs `main.ts` directly.
|
|
931
2986
|
|
|
932
2987
|
## API: @vestra-player/sdk
|
|
933
2988
|
|
|
@@ -1006,6 +3061,19 @@ change).
|
|
|
1006
3061
|
| `host` | Any other failure inside the player; `message` is the player's reason. |
|
|
1007
3062
|
| `disabled` | The plugin is being stopped; pending requests fail with it. |
|
|
1008
3063
|
| `unknown`, `rejected`, `expired` | After a transport problem the SDK could not confirm the outcome (does not happen in normal operation). |
|
|
3064
|
+
| `activation` | The plugin opened a tool window while its activation was still pending and the player could not track it. |
|
|
3065
|
+
| `command` | The player could not run or update a command it no longer knows for this plugin (removed meanwhile, or another plugin's). |
|
|
3066
|
+
| `input-overflow` | `openStream` input: the player's queue of physical key transitions was full; the input was revoked and its keys released. |
|
|
3067
|
+
| `internal` | An internal step of the player failed (keeping an operation's result, a subtitle or media job); `message` has the reason. |
|
|
3068
|
+
| `javascript`, `slint` | Packages.validate: the package's entry module does not pass `node --check` / one of its Slint files does not compile; `message` has the diagnostics. |
|
|
3069
|
+
| `owner` | A resolver answered a request that belongs to another plugin's resolver. |
|
|
3070
|
+
| `pending` | An operation with the same token is still running in the player. The SDK waits and asks again, so it rarely reaches a plugin. |
|
|
3071
|
+
| `producer` | `openStream`: the player refused or lost the generated stream; `message` has the reason. |
|
|
3072
|
+
| `resolution` | A file or web resolver's result could not be opened; `message` has the reason. |
|
|
3073
|
+
| `revisionConflict` | Packages: the installed revision changed since it was read (another install, rollback or uninstall ran). Read again and retry; the SDK passes the revision it read, so this reaches plugins that act on old data. |
|
|
3074
|
+
| `source-geometry`, `source-geometry-unsupported` | The source's picture size or rotation could not be applied: invalid decoder dimensions / a rotation that is not a multiple of 90°. |
|
|
3075
|
+
| `source-unavailable` | The source is not ready (still loading, or its time or native binding is not available yet). |
|
|
3076
|
+
| `unknown-expired` | The player no longer knows the operation or snapshot (it expired or was released). The SDK maps it to `unknownExpired` results where it can; do not resubmit the operation blindly. |
|
|
1009
3077
|
|
|
1010
3078
|
Errors thrown by handlers the SDK invokes go to Scope.onError. A
|
|
1011
3079
|
rejected promise you forgot to `await` is only logged; it never crashes
|
|
@@ -1174,16 +3242,19 @@ interface Plugin extends Scope {
|
|
|
1174
3242
|
/** The manifest id, e.g. `"community.bookmark"`. */
|
|
1175
3243
|
readonly id: string;
|
|
1176
3244
|
/**
|
|
1177
|
-
* A writable directory for this plugin's own files (settings,
|
|
1178
|
-
* folders)
|
|
1179
|
-
*
|
|
3245
|
+
* A writable directory for this plugin's own files (settings, caches,
|
|
3246
|
+
* remembered folders), created by the player before it starts the plugin.
|
|
3247
|
+
* Each installation has its own directory (player 0.2.0): it is kept across
|
|
3248
|
+
* restarts, disabling, upgrades and rollbacks, and a reinstall of the same
|
|
3249
|
+
* id from a different source gets a new, empty one. It is deleted only by
|
|
3250
|
+
* `packages.uninstall(id, { removeData: true })`. No capability.
|
|
1180
3251
|
*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
*
|
|
3252
|
+
* The path's format is not part of the API: do not derive another
|
|
3253
|
+
* plugin's directory from it (a plugin manager reads
|
|
3254
|
+
* PluginInfo.dataDirectory). Write files atomically (to a
|
|
3255
|
+
* temporary name, then rename); for JSON settings prefer
|
|
3256
|
+
* Scope.storage. Throws `unavailable` when the player provided
|
|
3257
|
+
* none. See How a plugin runs.
|
|
1187
3258
|
*/
|
|
1188
3259
|
readonly dataDirectory: string;
|
|
1189
3260
|
}
|
|
@@ -1245,9 +3316,10 @@ Playback 200, Subtitles 300, Video 400, Audio 500, Screen 710, Settings
|
|
|
1245
3316
|
item never moves the player's menu. Directly inside a built-in menu,
|
|
1246
3317
|
`order` 0 (the default) sorts after every other order: an item without
|
|
1247
3318
|
`order` goes at the end, after the player's own items (which all use 100
|
|
1248
|
-
or more). Give a positive order to sit between them (the player's
|
|
1249
|
-
|
|
1250
|
-
|
|
3319
|
+
or more). Give a positive order to sit between them (the player's items
|
|
3320
|
+
in menu order are in
|
|
3321
|
+
Built-in commands),
|
|
3322
|
+
or a negative one to come first. Deeper levels (a plugin's own submenu) have no such rule.
|
|
1251
3323
|
Players before 0.3.0 place every submenu at its smallest order, so there
|
|
1252
3324
|
an item without `order` moves the whole menu to the top; to avoid that,
|
|
1253
3325
|
give it an order in the menu's range (Video: 400–499).
|
|
@@ -1311,8 +3383,8 @@ interface CommandOptions {
|
|
|
1311
3383
|
*
|
|
1312
3384
|
* Platforms: on macOS `Ctrl+X` fires for both ⌃X and ⌘X while `Cmd+X`
|
|
1313
3385
|
* means ⌘X only; on Windows `Cmd` is the Windows logo key, so write
|
|
1314
|
-
* `Ctrl+…` for bindings that work on both. Avoid
|
|
1315
|
-
*
|
|
3386
|
+
* `Ctrl+…` for bindings that work on both. Avoid the
|
|
3387
|
+
* keys the system takes first.
|
|
1316
3388
|
*
|
|
1317
3389
|
* Conflicts are not errors: the user's overrides win, then the player's
|
|
1318
3390
|
* own commands, then the plugin loaded first; a shadowed binding takes
|
|
@@ -1605,8 +3677,10 @@ interface SlotState {
|
|
|
1605
3677
|
* - `fullscreen`: the sidebar's fullscreen overlay is collapsed and pops
|
|
1606
3678
|
* out when the pointer reaches the right edge; treat it as "on" for a
|
|
1607
3679
|
* toggle button;
|
|
1608
|
-
* - `idle`: the bottom bar in fullscreen
|
|
1609
|
-
*
|
|
3680
|
+
* - `idle`: the bottom bar in fullscreen while the pointer is outside the
|
|
3681
|
+
* bar's own band at the bottom of the screen (player 0.5.1: keys and
|
|
3682
|
+
* pointer motion elsewhere do not bring it back; before 0.5.1 it hid after
|
|
3683
|
+
* 3 seconds without activity).
|
|
1610
3684
|
*/
|
|
1611
3685
|
reason: "userHidden" | "space" | "fullscreen" | "idle" | null;
|
|
1612
3686
|
/** The sidebar floats in its own window; always `false` for the bottom bar. */
|
|
@@ -2874,10 +4948,15 @@ interface PlaybackStats {
|
|
|
2874
4948
|
backend: string;
|
|
2875
4949
|
/** The playback session number. */
|
|
2876
4950
|
session: number;
|
|
2877
|
-
/** Frames the
|
|
4951
|
+
/** Frames the video renderer produced. Not all of them reach the screen: a busy window shows only the newest one, so compare presentedFps. */
|
|
2878
4952
|
framesShown: number;
|
|
2879
|
-
/** Frames dropped, `null` when unknown. */
|
|
4953
|
+
/** Frames dropped by the backend (mpv), `null` when unknown. */
|
|
2880
4954
|
framesDropped: number | null;
|
|
4955
|
+
/**
|
|
4956
|
+
* New video frames the player window showed per second over the last second, measured by the player itself: draws of the main window that put a new picture on screen. Lower than `video.fps` when frames are lost after decoding and rendering (the window could not keep up with the source, or the display refreshes slower); `0` while paused. `null` when the player cannot measure it (no window, the first second, players before 0.5.1).
|
|
4957
|
+
* @player 0.5.1
|
|
4958
|
+
*/
|
|
4959
|
+
presentedFps: number | null;
|
|
2881
4960
|
/** Every field is the backend's own value, `null` when it has none. */
|
|
2882
4961
|
video: {
|
|
2883
4962
|
/** Codec name. */
|
|
@@ -2902,8 +4981,10 @@ interface PlaybackStats {
|
|
|
2902
4981
|
displayFps: number | null;
|
|
2903
4982
|
/** Bits per second, updated per keyframe. */
|
|
2904
4983
|
bitrate: number | null;
|
|
2905
|
-
/** Hardware decoder in use, e.g. "videotoolbox"; "no" = software decoding. */
|
|
4984
|
+
/** Hardware decoder in use, e.g. "videotoolbox", "d3d11va-copy"; "no" = software decoding. */
|
|
2906
4985
|
hwdec: string | null;
|
|
4986
|
+
/** How the video is decoded, from `hwdec`: `"hardware"` (the method is in `hwdec`) or `"software"`; `null` before a decoder runs. */
|
|
4987
|
+
decoding: "hardware" | "software" | null;
|
|
2907
4988
|
/** Transfer characteristic as mpv names it, e.g. "bt.1886", "pq", "hlg". */
|
|
2908
4989
|
transfer: string | null;
|
|
2909
4990
|
} | null;
|
|
@@ -4399,7 +6480,9 @@ type SubtitleStyle = {
|
|
|
4399
6480
|
outline: number;
|
|
4400
6481
|
/** Bottom margin 2–15 % of the video height. */
|
|
4401
6482
|
marginPercent: number;
|
|
4402
|
-
/**
|
|
6483
|
+
/**
|
|
6484
|
+
* Timing offset in milliseconds, −600000…600000 (±10 minutes); positive shows the subtitles later. Outside the range the player refuses the style with `invalid`.
|
|
6485
|
+
*/
|
|
4403
6486
|
offsetMs: number;
|
|
4404
6487
|
/** Bold text; default false. */
|
|
4405
6488
|
bold?: boolean;
|
|
@@ -5396,6 +7479,61 @@ function startPlugin(activate: (plugin: Plugin) => unknown, options?: {
|
|
|
5396
7479
|
}>;
|
|
5397
7480
|
```
|
|
5398
7481
|
|
|
7482
|
+
### CapabilityInfo
|
|
7483
|
+
|
|
7484
|
+
What one manifest capability is for: the members it opens, the
|
|
7485
|
+
capabilities they also need, and how the plugin store describes it. An
|
|
7486
|
+
entry of CAPABILITY_INFO.
|
|
7487
|
+
|
|
7488
|
+
```ts
|
|
7489
|
+
interface CapabilityInfo {
|
|
7490
|
+
/**
|
|
7491
|
+
* The members this capability opens, as `Interface.member` (for example `Commands.add` is `plugin.commands.add`). A call to one of them without the capability fails with code `capability`; `Plugin.video.filter` is `plugin.video.filter`. A member can be listed under more than one capability when it needs all of them (`Panel.onChange` needs `ui.mount` and `events`).
|
|
7492
|
+
*/
|
|
7493
|
+
readonly apis: readonly string[];
|
|
7494
|
+
/**
|
|
7495
|
+
* Capabilities every member in CapabilityInfo.apis also needs: declare them as well. What only one member or one option needs (`Plugin.video.filter` also needs `events`) is documented at that member.
|
|
7496
|
+
*/
|
|
7497
|
+
readonly requires: readonly string[];
|
|
7498
|
+
/**
|
|
7499
|
+
* How the plugin store and the player's install confirmation describe the capability to the user, in English (the player translates it). Two capabilities can share one description (`media.read` and `media.control`); the store then shows it once.
|
|
7500
|
+
*/
|
|
7501
|
+
readonly label: string;
|
|
7502
|
+
/**
|
|
7503
|
+
* `true` when the store highlights the capability as one to think about before installing (it reaches outside the plugin's own features: the clipboard, every shortcut, the keychain, other plugins).
|
|
7504
|
+
*/
|
|
7505
|
+
readonly risky: boolean;
|
|
7506
|
+
}
|
|
7507
|
+
```
|
|
7508
|
+
|
|
7509
|
+
### CAPABILITY_INFO
|
|
7510
|
+
|
|
7511
|
+
Every manifest capability the player knows (`vestra/1`), in the player's
|
|
7512
|
+
order, with the members it opens, the capabilities they also need and the
|
|
7513
|
+
store's wording. The player's tests check that the names agree with the
|
|
7514
|
+
player's `host/src/plugins.rs` and the labels with its plugin store.
|
|
7515
|
+
|
|
7516
|
+
A member not listed under any capability needs none (`plugin.tools`,
|
|
7517
|
+
`plugin.storage`, `plugin.system`, `plugin.app`, `plugin.ui`,
|
|
7518
|
+
`plugin.i18n`, reading `plugin.window.state`, `plugin.appearance.theme`
|
|
7519
|
+
and `plugin.packages.list`).
|
|
7520
|
+
|
|
7521
|
+
```ts
|
|
7522
|
+
const CAPABILITY_INFO: Readonly<Record<string, CapabilityInfo>>;
|
|
7523
|
+
```
|
|
7524
|
+
|
|
7525
|
+
### MESSAGE_LIMIT
|
|
7526
|
+
|
|
7527
|
+
The longest message a plugin may send the player, in UTF-8 bytes of one
|
|
7528
|
+
protocol line with its newline (1 MiB). The player stops a plugin that
|
|
7529
|
+
writes a longer one, so the plugin's side refuses such a request before
|
|
7530
|
+
sending it: it rejects with code `limit`. Property values meet
|
|
7531
|
+
UI_VALUE_LIMITS before this.
|
|
7532
|
+
|
|
7533
|
+
```ts
|
|
7534
|
+
const MESSAGE_LIMIT: number;
|
|
7535
|
+
```
|
|
7536
|
+
|
|
5399
7537
|
### COMMAND_LIMITS
|
|
5400
7538
|
|
|
5401
7539
|
The player's command registry caps, the same values as the player's own
|
|
@@ -5431,6 +7569,76 @@ agrees with the player's.
|
|
|
5431
7569
|
const MENU_IDS: readonly ["open", "playback", "subtitles", "video", "audio", "screen", "settings"];
|
|
5432
7570
|
```
|
|
5433
7571
|
|
|
7572
|
+
### BuiltinSubmenu
|
|
7573
|
+
|
|
7574
|
+
A submenu that one of the player's built-in plugins declares: an entry of
|
|
7575
|
+
BUILTIN_SUBMENUS.
|
|
7576
|
+
|
|
7577
|
+
```ts
|
|
7578
|
+
interface BuiltinSubmenu {
|
|
7579
|
+
/**
|
|
7580
|
+
* The submenu's key path: the English texts paths merge by, the built-in menu first by its key (`"Playback"` for `@playback`). Write these segments, followed by your command's title, to put a command into this submenu.
|
|
7581
|
+
*/
|
|
7582
|
+
readonly path: readonly string[];
|
|
7583
|
+
/** The built-in plugins that declare it (`vestra.history`). */
|
|
7584
|
+
readonly plugins: readonly string[];
|
|
7585
|
+
}
|
|
7586
|
+
```
|
|
7587
|
+
|
|
7588
|
+
### BUILTIN_SUBMENUS
|
|
7589
|
+
|
|
7590
|
+
The submenus the player's built-in plugins declare, by key path, in menu
|
|
7591
|
+
order (built-in menus as in MENU_IDS, then top-level submenus).
|
|
7592
|
+
Menu paths merge by their English key, so a command whose path starts
|
|
7593
|
+
with the same keys appears in the same submenu, whatever the interface
|
|
7594
|
+
language; the submenu's title is the declaring plugin's translation. The
|
|
7595
|
+
player's own submenus (`Playback ▸ Speed`, `Playback ▸ Seek`) are not
|
|
7596
|
+
listed. The player's tests check that this list agrees with what its
|
|
7597
|
+
built-in plugins declare.
|
|
7598
|
+
|
|
7599
|
+
```ts
|
|
7600
|
+
const BUILTIN_SUBMENUS: readonly BuiltinSubmenu[];
|
|
7601
|
+
```
|
|
7602
|
+
|
|
7603
|
+
### BuiltinCommand
|
|
7604
|
+
|
|
7605
|
+
A command the player or one of its built-in plugins registers: an entry
|
|
7606
|
+
of BUILTIN_COMMANDS.
|
|
7607
|
+
|
|
7608
|
+
```ts
|
|
7609
|
+
interface BuiltinCommand {
|
|
7610
|
+
/** `"host"` for the player's own commands, else the built-in plugin's id. */
|
|
7611
|
+
readonly owner: string;
|
|
7612
|
+
/** The command's id within its owner (`"play-pause"`, `"capture.save"`). */
|
|
7613
|
+
readonly id: string;
|
|
7614
|
+
/**
|
|
7615
|
+
* The menu path by key (the English texts paths merge by), the leaf included; a built-in menu by its key (`"Video"`, not `"@video"`). Empty for a command that is not in the main menu (a panel shortcut).
|
|
7616
|
+
*/
|
|
7617
|
+
readonly path: readonly string[];
|
|
7618
|
+
/** The English title (the last segment of `path` when there is one). */
|
|
7619
|
+
readonly title: string;
|
|
7620
|
+
/** The default bindings, canonical, in the order the command declares them. */
|
|
7621
|
+
readonly bindings: readonly string[];
|
|
7622
|
+
/** `"panel"`: the bindings apply only while the owner's panel has focus. */
|
|
7623
|
+
readonly focus: "panel" | null;
|
|
7624
|
+
}
|
|
7625
|
+
```
|
|
7626
|
+
|
|
7627
|
+
### BUILTIN_COMMANDS
|
|
7628
|
+
|
|
7629
|
+
The commands the player and its built-in plugins register when the
|
|
7630
|
+
player starts with nothing open, in menu order (commands outside the main
|
|
7631
|
+
menu last): their key paths, default bindings and owners. Use it to pick
|
|
7632
|
+
a menu place and default keys that do not collide with the player's (the
|
|
7633
|
+
user's own changes come on top; see Bindings). Menu items built
|
|
7634
|
+
from content (tracks, recent files, output devices) are not listed; their
|
|
7635
|
+
placeholders (`No Audio Tracks`) are. The player's tests check that this
|
|
7636
|
+
list agrees with what the player and its built-in plugins register.
|
|
7637
|
+
|
|
7638
|
+
```ts
|
|
7639
|
+
const BUILTIN_COMMANDS: readonly BuiltinCommand[];
|
|
7640
|
+
```
|
|
7641
|
+
|
|
5434
7642
|
### LOCALES
|
|
5435
7643
|
|
|
5436
7644
|
The player's interface languages: English (the source language) first,
|
|
@@ -5472,6 +7680,95 @@ const TEXT_LIMITS: {
|
|
|
5472
7680
|
};
|
|
5473
7681
|
```
|
|
5474
7682
|
|
|
7683
|
+
### THEME_PALETTES
|
|
7684
|
+
|
|
7685
|
+
The colours of the player's palettes, by `Theme` colour token (without the
|
|
7686
|
+
`Theme.` prefix): `vestra`, the dark default, and `vestra-light` (player
|
|
7687
|
+
0.4.0). The player sets one of them on every view; a plugin reads the
|
|
7688
|
+
colours through `Theme` in its Slint files and never needs these values.
|
|
7689
|
+
They are here for the documentation (Reference: theme) and for tests that
|
|
7690
|
+
check a design against both palettes.
|
|
7691
|
+
|
|
7692
|
+
Current values: a later player may adjust them, a token keeps its
|
|
7693
|
+
meaning. The `vestra` values are also the defaults of the SDK's copy of
|
|
7694
|
+
the palette (`slint/palette.slint`); the player's tests check both
|
|
7695
|
+
palettes against its own.
|
|
7696
|
+
|
|
7697
|
+
```ts
|
|
7698
|
+
const THEME_PALETTES: Readonly<Record<"vestra" | "vestra-light", Readonly<Record<string, string>>>>;
|
|
7699
|
+
```
|
|
7700
|
+
|
|
7701
|
+
### BINDING_INPUTS
|
|
7702
|
+
|
|
7703
|
+
The names a binding (`CommandOptions.bindings`, `Bindings.set`) is made
|
|
7704
|
+
of, as the player's binding parser accepts them. A binding is
|
|
7705
|
+
`{ modifier "+" } input`: modifiers in the order of `modifiers`, each at
|
|
7706
|
+
most once, then one input. An input is a name of `letters`, `digits`,
|
|
7707
|
+
`functionKeys`, `named` or `mouse`, or one other printable character that
|
|
7708
|
+
is not a letter, digit, space or `+` (`">"`, `"["`, `"/"`), written as
|
|
7709
|
+
typed, never with `Shift` (`">"`, not `"Shift+."`; `"!"`, not
|
|
7710
|
+
`"Shift+1"`). Anything else fails with code `invalid`. The player's tests
|
|
7711
|
+
check that these agree with the player.
|
|
7712
|
+
|
|
7713
|
+
```ts
|
|
7714
|
+
const BINDING_INPUTS: Readonly<{
|
|
7715
|
+
/** Modifiers, in the order a binding writes them. `Cmd` is ⌘ on macOS and the Windows logo key on Windows; on macOS `Ctrl` also matches ⌘. */
|
|
7716
|
+
readonly modifiers: readonly ["Ctrl", "Alt", "Shift", "Cmd"];
|
|
7717
|
+
/** Letter keys, by key name whatever the case: `A` to `Z`. */
|
|
7718
|
+
readonly letters: readonly string[];
|
|
7719
|
+
/** Digit keys of the main keyboard: `0` to `9`. */
|
|
7720
|
+
readonly digits: readonly string[];
|
|
7721
|
+
/** Function keys: `F1` to `F24`. */
|
|
7722
|
+
readonly functionKeys: string[];
|
|
7723
|
+
/**
|
|
7724
|
+
* Other named keys, matched by the physical key. `Plus` is the `+` key however it is typed; the `Numpad…` names are the numeric keypad only, never the main keyboard.
|
|
7725
|
+
*/
|
|
7726
|
+
readonly named: readonly ["Space", "Enter", "Tab", "Escape", "Backspace", "Delete", "Insert", "Home", "End", "PageUp", "PageDown", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Plus", ...string[], "NumpadPlus", "NumpadMinus", "NumpadMultiply", "NumpadDivide", "NumpadEnter", "NumpadDecimal"];
|
|
7727
|
+
/** Mouse inputs on the video area. `:LeftHalf` and `:RightHalf` tell the halves of the video apart for a double click. */
|
|
7728
|
+
readonly mouse: readonly ["Mouse:LeftClick", "Mouse:LeftDoubleClick", "Mouse:LeftDoubleClick:LeftHalf", "Mouse:LeftDoubleClick:RightHalf", "Mouse:MiddleClick", "Mouse:X1", "Mouse:X2", "Mouse:WheelUp", "Mouse:WheelDown", "Mouse:WheelLeft", "Mouse:WheelRight"];
|
|
7729
|
+
}>;
|
|
7730
|
+
```
|
|
7731
|
+
|
|
7732
|
+
### OSD_LIMITS
|
|
7733
|
+
|
|
7734
|
+
The player's limits on the on-screen display (Osd), counted on
|
|
7735
|
+
the text as shown: after the player translated it and filled in its
|
|
7736
|
+
arguments. A text over them fails with code `invalid`: `osd.show`
|
|
7737
|
+
rejects, `OsdPanel.set` reports to `Scope.onError`. The player's tests
|
|
7738
|
+
check that these agree with the player.
|
|
7739
|
+
|
|
7740
|
+
```ts
|
|
7741
|
+
const OSD_LIMITS: Readonly<{
|
|
7742
|
+
/** Characters of one `osd.show` message (200; at least 1 that is not white space). Line breaks are shown as spaces. */
|
|
7743
|
+
readonly messageChars: 200;
|
|
7744
|
+
/** Lines of the information block (`OsdPanel.set`) (32). */
|
|
7745
|
+
readonly panelLines: 32;
|
|
7746
|
+
/** UTF-8 bytes of the information block (4096). */
|
|
7747
|
+
readonly panelBytes: 4096;
|
|
7748
|
+
/** The smallest `OsdPanelOptions.maxColumns` (8). */
|
|
7749
|
+
readonly minColumns: 8;
|
|
7750
|
+
/** The largest `OsdPanelOptions.maxColumns` (1000). */
|
|
7751
|
+
readonly maxColumns: 1000;
|
|
7752
|
+
}>;
|
|
7753
|
+
```
|
|
7754
|
+
|
|
7755
|
+
### SECRET_LIMITS
|
|
7756
|
+
|
|
7757
|
+
The player's limits on Secrets. A name or value outside them
|
|
7758
|
+
fails with code `invalid`; a new name beyond `names` fails with `limit`.
|
|
7759
|
+
The player's tests check that these agree with the player.
|
|
7760
|
+
|
|
7761
|
+
```ts
|
|
7762
|
+
const SECRET_LIMITS: Readonly<{
|
|
7763
|
+
/** Characters of a name (64, at least 1), each one of `a-z`, `0-9`, `.`, `_` and `-`. */
|
|
7764
|
+
readonly nameChars: 64;
|
|
7765
|
+
/** UTF-8 bytes of a value (1024, at least 1; no NUL character). */
|
|
7766
|
+
readonly valueBytes: 1024;
|
|
7767
|
+
/** Different names one plugin may have stored at the same time (256). */
|
|
7768
|
+
readonly names: 256;
|
|
7769
|
+
}>;
|
|
7770
|
+
```
|
|
7771
|
+
|
|
5475
7772
|
### UI_VALUE_LIMITS
|
|
5476
7773
|
|
|
5477
7774
|
The player's limits on one property value (a `PropValue` as written
|
|
@@ -5740,10 +8037,15 @@ interface PlaybackStats {
|
|
|
5740
8037
|
backend: string;
|
|
5741
8038
|
/** The playback session number. */
|
|
5742
8039
|
session: number;
|
|
5743
|
-
/** Frames the
|
|
8040
|
+
/** Frames the video renderer produced. Not all of them reach the screen: a busy window shows only the newest one, so compare presentedFps. */
|
|
5744
8041
|
framesShown: number;
|
|
5745
|
-
/** Frames dropped, `null` when unknown. */
|
|
8042
|
+
/** Frames dropped by the backend (mpv), `null` when unknown. */
|
|
5746
8043
|
framesDropped: number | null;
|
|
8044
|
+
/**
|
|
8045
|
+
* New video frames the player window showed per second over the last second, measured by the player itself: draws of the main window that put a new picture on screen. Lower than `video.fps` when frames are lost after decoding and rendering (the window could not keep up with the source, or the display refreshes slower); `0` while paused. `null` when the player cannot measure it (no window, the first second, players before 0.5.1).
|
|
8046
|
+
* @player 0.5.1
|
|
8047
|
+
*/
|
|
8048
|
+
presentedFps: number | null;
|
|
5747
8049
|
/** Every field is the backend's own value, `null` when it has none. */
|
|
5748
8050
|
video: {
|
|
5749
8051
|
/** Codec name. */
|
|
@@ -5768,8 +8070,10 @@ interface PlaybackStats {
|
|
|
5768
8070
|
displayFps: number | null;
|
|
5769
8071
|
/** Bits per second, updated per keyframe. */
|
|
5770
8072
|
bitrate: number | null;
|
|
5771
|
-
/** Hardware decoder in use, e.g. "videotoolbox"; "no" = software decoding. */
|
|
8073
|
+
/** Hardware decoder in use, e.g. "videotoolbox", "d3d11va-copy"; "no" = software decoding. */
|
|
5772
8074
|
hwdec: string | null;
|
|
8075
|
+
/** How the video is decoded, from `hwdec`: `"hardware"` (the method is in `hwdec`) or `"software"`; `null` before a decoder runs. */
|
|
8076
|
+
decoding: "hardware" | "software" | null;
|
|
5773
8077
|
/** Transfer characteristic as mpv names it, e.g. "bt.1886", "pq", "hlg". */
|
|
5774
8078
|
transfer: string | null;
|
|
5775
8079
|
} | null;
|
|
@@ -6105,7 +8409,7 @@ The SDK is imported with `import type` only: the player passes the `plugin` obje
|
|
|
6105
8409
|
|
|
6106
8410
|
(code: `main.ts`, part "command", below)
|
|
6107
8411
|
|
|
6108
|
-
- `path: ["@video", "Blur…"]` puts the item in the player's own **Video** menu.
|
|
8412
|
+
- `path: ["@video", "Blur…"]` puts the item in the player's own **Video** menu. Paths merge by their English keys in every interface language, so `["Video", "Blur…"]` reaches the same menu; the id form `@video` says plainly that a built-in menu is meant, and an unknown id fails with `invalid` instead of making a new menu (Menu paths). Players up to 0.4.2 titled their menus in Chinese and are reached only by the id. The built-in menu stays where it is, and an item without `order` goes at the end of it.
|
|
6109
8413
|
- `run` opens the window, or shows it again after the user closed it. Returning the promise of `show()` lets the SDK report a failure to the plugin log.
|
|
6110
8414
|
|
|
6111
8415
|
#### ui/blur.slint
|
|
@@ -6410,7 +8714,7 @@ When the plugin is disabled or the player quits, the SDK stops the stream the sa
|
|
|
6410
8714
|
|
|
6411
8715
|
(code: `main.ts`, part "command", below)
|
|
6412
8716
|
|
|
6413
|
-
- `path: ["@open", "Signal generator"]` puts the item in the player's own **Open** menu.
|
|
8717
|
+
- `path: ["@open", "Signal generator"]` puts the item in the player's own **Open** menu. Paths merge by their English keys in every interface language, so `["Open", …]` reaches the same menu; the id form `@open` names the built-in menu explicitly and also reaches it in players up to 0.4.2, whose menus were titled in Chinese (Menu paths).
|
|
6414
8718
|
- `checked` is a function. The SDK calls it again after every handler and every `onStop`, and updates the check mark when the result changes.
|
|
6415
8719
|
- `run` starts the stream when there is none and stops it when there is one. It returns the promise so that a failure reaches the plugin log.
|
|
6416
8720
|
|
|
@@ -7370,13 +9674,13 @@ A window that shows a formatted Markdown document with the player's `MarkdownVie
|
|
|
7370
9674
|
|
|
7371
9675
|
- `commands` for the menu item, `ui.window` for the window. `plugin.ui.markdown` needs no capability.
|
|
7372
9676
|
- `minPlayerVersion: "0.2.0"`: `ui.markdown` is new in that version, and older players refuse the plugin with a clear message.
|
|
7373
|
-
- `name` to `license` are display fields for the plugin manager. They do not affect how the plugin runs, and any plugin can have them;
|
|
9677
|
+
- `name` to `license` are display fields for the plugin manager. They do not affect how the plugin runs, and any plugin can have them; Manifest lists their rules.
|
|
7374
9678
|
|
|
7375
9679
|
#### The package file
|
|
7376
9680
|
|
|
7377
9681
|
(code: `package.json`, below)
|
|
7378
9682
|
|
|
7379
|
-
`vestra-plugin build` leaves Markdown files out of the package (except the
|
|
9683
|
+
`vestra-plugin build` leaves Markdown files out of the package (except `README.md`, `CHANGELOG.md` and the manifest's `readme` and `changelog`; see What build puts into dist/), so `sample.md` is listed under `"vestra": { "assets": [...] }`, which copies files into the package as they are. `assets/icon.png` needs no entry: the build copies images anyway.
|
|
7380
9684
|
|
|
7381
9685
|
#### The window
|
|
7382
9686
|
|
|
@@ -7416,7 +9720,8 @@ host.markdown() reads the document back from the window's property: its `text` a
|
|
|
7416
9720
|
|
|
7417
9721
|
1. **Open links.** Add `callback open-link(string);` to the window, forward `link-clicked(url) => { root.open-link(url); }` from `MarkdownView`, and handle it with `window.on("openLink", (url: string) => plugin.system.openUrl(url))` before the first `show()`.
|
|
7418
9722
|
2. **Remote images.** Put `` in the document: `ui.markdown` downloads `https:` images first (pass `remoteImages: false` to turn that off).
|
|
7419
|
-
3. **
|
|
9723
|
+
3. **Copy code.** Set `code-copy: true` on `MarkdownView`: code blocks get a **Copy** button (labelled in the interface language) that reports `copy-code(code)`. Forward it as a callback and copy the text with plugin.clipboard, which needs the `clipboard` capability.
|
|
9724
|
+
4. **Show the plugin's own README** instead of `sample.md`; the build always packs the README.
|
|
7420
9725
|
|
|
7421
9726
|
#### examples/markdown/manifest.json
|
|
7422
9727
|
|