@vestra-player/sdk 1.5.0 → 1.5.1
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 +9 -0
- package/README.md +29 -22
- package/dist/plugin/services.js +61 -11
- package/dist/plugin/types.d.ts +82 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.1
|
|
4
|
+
|
|
5
|
+
Fix; no API or type changes.
|
|
6
|
+
|
|
7
|
+
- **`storage.json` saves no longer collide.** Every save wrote through the same temporary file (`<name>.json.tmp-<pid>`), and only the writes of one store were queued. Two stores on the same file (the same name opened twice, in one scope or two) could therefore rename each other's temporary file away mid-save, and the save failed with `ENOENT` (on `chmod` or `rename`); on Windows two renames onto the file at once also fail with `EPERM`. Seen intermittently on Windows.
|
|
8
|
+
- Writes to one file are now queued together, whichever store makes them, and land in the order they were made.
|
|
9
|
+
- Each write has a temporary file of its own, made owner-only (`0600`) through its file handle before it gets the store's name, and removed again if the write fails.
|
|
10
|
+
- On Windows, replacing the file is retried for up to 2 s while another program (a virus scanner, the search indexer) briefly holds it open.
|
|
11
|
+
|
|
3
12
|
## 1.5.0
|
|
4
13
|
|
|
5
14
|
**Own visual identity: the Aurora palettes and the V-Line icons** (player 0.4.0). Additive; `apiVersion` stays `vestra/1`. Plugins that use the additions set `"minPlayerVersion": "0.4.0"`.
|
package/README.md
CHANGED
|
@@ -33,7 +33,13 @@ npm i -D @vestra-player/sdk # Node.js 26 or newer
|
|
|
33
33
|
`manifest.json` and `main.ts`:
|
|
34
34
|
|
|
35
35
|
```json
|
|
36
|
-
{
|
|
36
|
+
{
|
|
37
|
+
"id": "examples.hello",
|
|
38
|
+
"version": "0.1.0",
|
|
39
|
+
"apiVersion": "vestra/1",
|
|
40
|
+
"entry": "main.ts",
|
|
41
|
+
"capabilities": ["commands", "ui.osd"]
|
|
42
|
+
}
|
|
37
43
|
```
|
|
38
44
|
|
|
39
45
|
```ts
|
|
@@ -54,33 +60,34 @@ Run `npx vestra-plugin pack` to check and package it, or start the player with `
|
|
|
54
60
|
|
|
55
61
|
Complete, tested plugins in [`examples/`](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples). The [examples page](https://vestrasys.github.io/vestra-player-sdk/documents/Examples.html) shows each one with a screenshot and a step-by-step walkthrough.
|
|
56
62
|
|
|
57
|
-
| Example
|
|
58
|
-
|
|
|
59
|
-
| [hello](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/hello)
|
|
60
|
-
| [blur](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/blur)
|
|
61
|
-
| [signal-generator](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/signal-generator) | Generating video and audio frames yourself
|
|
62
|
-
| [bookmark](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/bookmark)
|
|
63
|
-
| [panel-menu](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/panel-menu)
|
|
64
|
-
| [
|
|
65
|
-
| [
|
|
63
|
+
| Example | What you learn | Walkthrough |
|
|
64
|
+
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
|
|
65
|
+
| [hello](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/hello) | The smallest plugin: a manifest, one menu command, an on-screen message. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Hello.html) |
|
|
66
|
+
| [blur](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/blur) | A video filter driven by one slider in a tool window. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Blur.html) |
|
|
67
|
+
| [signal-generator](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/signal-generator) | Generating video and audio frames yourself: colour bars and a sine tone. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Signal_generator.html) |
|
|
68
|
+
| [bookmark](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/bookmark) | A tool window bound to the playback state, with Mark and Jump buttons. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Bookmark.html) |
|
|
69
|
+
| [panel-menu](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/panel-menu) | A sidebar panel with its own right-click menu. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Panel_menu.html) |
|
|
70
|
+
| [menus](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/menus) | Radio items, separator groups and a menu item whose title changes. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Menus.html) |
|
|
71
|
+
| [theme-widgets](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/theme-widgets) | A tool window drawn with the player's theme and widgets. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Theme_widgets.html) |
|
|
72
|
+
| [markdown](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples/markdown) | A window that shows a Markdown document. | [Step by step](https://vestrasys.github.io/vestra-player-sdk/documents/Example__Markdown.html) |
|
|
66
73
|
|
|
67
74
|
## Command line
|
|
68
75
|
|
|
69
|
-
| Command
|
|
70
|
-
|
|
|
71
|
-
| `vestra-plugin check [dir...]`
|
|
72
|
-
| `vestra-plugin test [dir...]`
|
|
73
|
-
| `vestra-plugin build [dir...] [--version x.y.z]`
|
|
74
|
-
| `vestra-plugin pack [dir...] [--version x.y.z] [--out dir]` | Builds, then packs `dist/` as `<id>-<version>.vxp`.
|
|
76
|
+
| Command | Does |
|
|
77
|
+
| ----------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
78
|
+
| `vestra-plugin check [dir...]` | Checks the manifest, then a strict type check of the plugin and its tests. |
|
|
79
|
+
| `vestra-plugin test [dir...]` | Runs `node --test` on `tests/*.test.{ts,mjs}`. |
|
|
80
|
+
| `vestra-plugin build [dir...] [--version x.y.z]` | Writes `dist/`: types stripped, files copied, manifest versioned. |
|
|
81
|
+
| `vestra-plugin pack [dir...] [--version x.y.z] [--out dir]` | Builds, then packs `dist/` as `<id>-<version>.vxp`. |
|
|
75
82
|
|
|
76
83
|
## Compatibility
|
|
77
84
|
|
|
78
|
-
| SDK | Plugin API | Vestra Player
|
|
79
|
-
| --- |
|
|
80
|
-
| 1.0 | `vestra/1` | 0.1 or newer
|
|
81
|
-
| 1.1 | `vestra/1` | 0.2 or newer for the 1.1 additions
|
|
82
|
-
| 1.2 | `vestra/1` | 0.2 or newer
|
|
83
|
-
| 1.3 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions (menu ids, `Theme.danger`, empty video chains)
|
|
85
|
+
| SDK | Plugin API | Vestra Player | Node.js |
|
|
86
|
+
| --- | ---------- | ----------------------------------------------------------------------------------------- | ---------------------------- |
|
|
87
|
+
| 1.0 | `vestra/1` | 0.1 or newer | 22.18 or newer |
|
|
88
|
+
| 1.1 | `vestra/1` | 0.2 or newer for the 1.1 additions | 22.18 or newer |
|
|
89
|
+
| 1.2 | `vestra/1` | 0.2 or newer | 26 (shipped with the player) |
|
|
90
|
+
| 1.3 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions (menu ids, `Theme.danger`, empty video chains) | 26 (shipped with the player) |
|
|
84
91
|
| 1.4 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions. `ThemeId` is `"vestra"` only (see the CHANGELOG) | 26 (shipped with the player) |
|
|
85
92
|
| 1.5 | `vestra/1` | 0.2 or newer; 0.3 for the 1.3 additions; 0.4 for the 1.5 additions (`vestra-light`, `@vestra/icons.slint`) | 26 (shipped with the player) |
|
|
86
93
|
|
package/dist/plugin/services.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* run in this plugin's own process.
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, readFileSync, renameSync } from "node:fs";
|
|
7
|
-
import {
|
|
7
|
+
import { mkdir, open, rename, rm, stat } from "node:fs/promises";
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
9
|
import { failure } from "./runtime.js";
|
|
10
10
|
import { systemServices } from "./local.js";
|
|
@@ -101,6 +101,54 @@ export function tools(scope) {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
const NAME = /^[a-z0-9-]{1,64}$/;
|
|
104
|
+
/** Writes so far in this process; numbers each write's temporary file. */
|
|
105
|
+
let written = 0;
|
|
106
|
+
/**
|
|
107
|
+
* The pending writes of each file, whichever store they come from: the same
|
|
108
|
+
* name opened twice must not write or rename into one file at once.
|
|
109
|
+
*/
|
|
110
|
+
const writes = new Map();
|
|
111
|
+
/** Write `text` to `path` atomically, owner-only (0600). */
|
|
112
|
+
async function writeAtomic(path, text) {
|
|
113
|
+
// A name of its own per write, so no other write renames it away.
|
|
114
|
+
const temporary = `${path}.tmp-${process.pid}-${++written}`;
|
|
115
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
116
|
+
try {
|
|
117
|
+
// Settings can hold private paths and history. The mode is set through
|
|
118
|
+
// the handle, before the file has its name.
|
|
119
|
+
const file = await open(temporary, "w", 0o600);
|
|
120
|
+
try {
|
|
121
|
+
await file.chmod(0o600);
|
|
122
|
+
await file.writeFile(text);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
await file.close();
|
|
126
|
+
}
|
|
127
|
+
await replace(temporary, path);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
await rm(temporary, { force: true }).catch(() => { });
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Rename over `to`. Windows refuses while another program (a virus scanner,
|
|
136
|
+
* the search indexer) has `to` open for a moment: retry for up to 2 s there.
|
|
137
|
+
*/
|
|
138
|
+
async function replace(from, to) {
|
|
139
|
+
for (let attempt = 0;; attempt++)
|
|
140
|
+
try {
|
|
141
|
+
return await rename(from, to);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const code = error.code;
|
|
145
|
+
if (process.platform !== "win32" ||
|
|
146
|
+
!(code === "EPERM" || code === "EACCES" || code === "EBUSY") ||
|
|
147
|
+
attempt >= 20)
|
|
148
|
+
throw error;
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, 10 + attempt * 10));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
104
152
|
class Store {
|
|
105
153
|
#scope;
|
|
106
154
|
#path;
|
|
@@ -159,17 +207,19 @@ class Store {
|
|
|
159
207
|
return this.#writing;
|
|
160
208
|
this.#dirty = false;
|
|
161
209
|
const text = `${JSON.stringify(this.#value, null, 2)}\n`;
|
|
162
|
-
// One write at a time, in order; the newest value is written
|
|
163
|
-
|
|
210
|
+
// One write per file at a time, in order; the newest value is written
|
|
211
|
+
// last. The queue is per file, not per store (see `writes`).
|
|
212
|
+
const path = this.#path;
|
|
213
|
+
const write = (writes.get(path) ?? Promise.resolve())
|
|
164
214
|
.catch(() => { })
|
|
165
|
-
.then(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
215
|
+
.then(() => writeAtomic(path, text));
|
|
216
|
+
const done = () => {
|
|
217
|
+
if (writes.get(path) === write)
|
|
218
|
+
writes.delete(path);
|
|
219
|
+
};
|
|
220
|
+
writes.set(path, write);
|
|
221
|
+
write.then(done, done);
|
|
222
|
+
this.#writing = write;
|
|
173
223
|
return this.#writing;
|
|
174
224
|
}
|
|
175
225
|
async dispose() {
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -1229,8 +1229,17 @@ export interface SlotState {
|
|
|
1229
1229
|
*
|
|
1230
1230
|
* @capability ui.mount; `events` to follow changes made elsewhere.
|
|
1231
1231
|
*
|
|
1232
|
-
* @example A "Show sidebar" menu item that works with any sidebar panel
|
|
1233
|
-
*
|
|
1232
|
+
* @example A "Show sidebar" menu item that works with any sidebar panel
|
|
1233
|
+
* ```ts
|
|
1234
|
+
* const sidebar = plugin.panels.sidebar;
|
|
1235
|
+
* plugin.commands.add({
|
|
1236
|
+
* id: "sidebar",
|
|
1237
|
+
* path: ["Tools", "Show sidebar"],
|
|
1238
|
+
* checked: () => sidebar.state?.preferred ?? false,
|
|
1239
|
+
* enabled: () => sidebar.state !== null,
|
|
1240
|
+
* run: () => sidebar.toggle(),
|
|
1241
|
+
* });
|
|
1242
|
+
* ```
|
|
1234
1243
|
*
|
|
1235
1244
|
* @see {@link Panels}, {@link SlotState}
|
|
1236
1245
|
* @since 1.0.0
|
|
@@ -1330,8 +1339,17 @@ export interface PanelSlot {
|
|
|
1330
1339
|
*
|
|
1331
1340
|
* @capability ui.mount
|
|
1332
1341
|
*
|
|
1333
|
-
* @example
|
|
1334
|
-
*
|
|
1342
|
+
* @example A "Detach sidebar" menu item
|
|
1343
|
+
* ```ts
|
|
1344
|
+
* const sidebar = plugin.panels.sidebar;
|
|
1345
|
+
* plugin.commands.add({
|
|
1346
|
+
* id: "detach",
|
|
1347
|
+
* path: ["Tools", "Detach sidebar"],
|
|
1348
|
+
* checked: () => sidebar.state?.detached ?? false,
|
|
1349
|
+
* enabled: () => sidebar.state !== null,
|
|
1350
|
+
* run: () => sidebar.setDetached(!sidebar.state?.detached),
|
|
1351
|
+
* });
|
|
1352
|
+
* ```
|
|
1335
1353
|
*
|
|
1336
1354
|
* @since 1.0.0
|
|
1337
1355
|
* @category UI
|
|
@@ -1714,12 +1732,59 @@ export type SettingsCategory = "general" | "playback" | "subtitles" | "video" |
|
|
|
1714
1732
|
*
|
|
1715
1733
|
* @capability ui.settings
|
|
1716
1734
|
*
|
|
1717
|
-
* @example Two preferences on a page
|
|
1718
|
-
*
|
|
1735
|
+
* @example Two preferences on a page, kept in a store
|
|
1736
|
+
* ```ts
|
|
1737
|
+
* const store = plugin.storage.json("blur", {
|
|
1738
|
+
* defaults: { restoreOnStart: false, messages: true },
|
|
1739
|
+
* });
|
|
1740
|
+
* const page = plugin.settings.page("ui/settings.slint", {
|
|
1741
|
+
* id: "blur",
|
|
1742
|
+
* category: "video",
|
|
1743
|
+
* title: "Blur",
|
|
1744
|
+
* });
|
|
1745
|
+
* page.bind(() => ({
|
|
1746
|
+
* restoreOnStart: store.value.restoreOnStart,
|
|
1747
|
+
* messages: store.value.messages,
|
|
1748
|
+
* }));
|
|
1749
|
+
* page.on("restoreToggled", (on: boolean) =>
|
|
1750
|
+
* store.save({ ...store.value, restoreOnStart: on }),
|
|
1751
|
+
* );
|
|
1752
|
+
* page.on("messagesToggled", (on: boolean) =>
|
|
1753
|
+
* store.save({ ...store.value, messages: on }),
|
|
1754
|
+
* );
|
|
1755
|
+
* ```
|
|
1756
|
+
*
|
|
1757
|
+
* `ui/settings.slint` exports the bridge global and the layout:
|
|
1719
1758
|
*
|
|
1720
|
-
*
|
|
1759
|
+
* ```slint
|
|
1760
|
+
* import { CheckBox } from "std-widgets.slint";
|
|
1761
|
+
* import { Theme } from "@vestra/theme.slint";
|
|
1721
1762
|
*
|
|
1722
|
-
* {
|
|
1763
|
+
* export global BlurPage {
|
|
1764
|
+
* in-out property <bool> restore-on-start;
|
|
1765
|
+
* in-out property <bool> messages: true;
|
|
1766
|
+
* callback restore-toggled(bool);
|
|
1767
|
+
* callback messages-toggled(bool);
|
|
1768
|
+
* }
|
|
1769
|
+
*
|
|
1770
|
+
* export component BlurSettings inherits Rectangle {
|
|
1771
|
+
* VerticalLayout {
|
|
1772
|
+
* padding: Theme.dialog-padding;
|
|
1773
|
+
* spacing: 6px;
|
|
1774
|
+
* alignment: start;
|
|
1775
|
+
* CheckBox {
|
|
1776
|
+
* text: "Turn blur back on when the player starts";
|
|
1777
|
+
* checked <=> BlurPage.restore-on-start;
|
|
1778
|
+
* toggled => { BlurPage.restore-toggled(self.checked); }
|
|
1779
|
+
* }
|
|
1780
|
+
* CheckBox {
|
|
1781
|
+
* text: "Show on-screen messages";
|
|
1782
|
+
* checked <=> BlurPage.messages;
|
|
1783
|
+
* toggled => { BlurPage.messages-toggled(self.checked); }
|
|
1784
|
+
* }
|
|
1785
|
+
* }
|
|
1786
|
+
* }
|
|
1787
|
+
* ```
|
|
1723
1788
|
*
|
|
1724
1789
|
* @see {@link Settings.page}, [Windows, panels, settings pages, dialogs and the OSD](../../docs/concepts/ui.md)
|
|
1725
1790
|
* @since 1.0.0
|
|
@@ -1854,7 +1919,7 @@ export interface Settings {
|
|
|
1854
1919
|
* @throws `capability` (rejects) The manifest does not declare
|
|
1855
1920
|
* `ui.settings`.
|
|
1856
1921
|
*
|
|
1857
|
-
* @example An "Options…" button in a tool window
|
|
1922
|
+
* @example An "Options…" button in a tool window
|
|
1858
1923
|
* ```ts
|
|
1859
1924
|
* const controls = plugin.windows.create("ui/window.slint");
|
|
1860
1925
|
* controls.on("openOptions", () => plugin.settings.show("blur"));
|
|
@@ -3051,8 +3116,14 @@ export interface OsdPanel extends Disposable {
|
|
|
3051
3116
|
*
|
|
3052
3117
|
* @capability ui.osd
|
|
3053
3118
|
*
|
|
3054
|
-
* @example On-screen messages that the user can turn off
|
|
3055
|
-
*
|
|
3119
|
+
* @example On-screen messages that the user can turn off
|
|
3120
|
+
* ```ts
|
|
3121
|
+
* const store = plugin.storage.json("prefs", { defaults: { messages: true } });
|
|
3122
|
+
* const say = (text: string) => {
|
|
3123
|
+
* if (store.value.messages) void plugin.osd.show(text);
|
|
3124
|
+
* };
|
|
3125
|
+
* say("Blur on");
|
|
3126
|
+
* ```
|
|
3056
3127
|
*
|
|
3057
3128
|
* @see {@link OsdPanel}, [Windows, panels, settings pages, dialogs and the OSD](../../docs/concepts/ui.md)
|
|
3058
3129
|
* @since 1.0.0
|