@vestra-player/sdk 1.6.18 → 1.6.19
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 +11 -0
- package/dist/docs-tree.json.gz +0 -0
- package/dist/plugin/appearance.js +1 -1
- package/dist/plugin/bindings.js +1 -1
- package/dist/plugin/commands.js +3 -2
- package/dist/plugin/connection.d.ts +119 -0
- package/dist/plugin/connection.js +402 -0
- package/dist/plugin/drops.d.ts +3 -3
- package/dist/plugin/drops.js +1 -1
- package/dist/plugin/errors.d.ts +9 -0
- package/dist/plugin/errors.js +35 -0
- package/dist/plugin/filters.js +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.js +3 -1
- package/dist/plugin/language.js +1 -1
- package/dist/plugin/latest.d.ts +12 -0
- package/dist/plugin/latest.js +52 -0
- package/dist/plugin/local.js +1 -2
- package/dist/plugin/markdown.d.ts +2 -2
- package/dist/plugin/markdown.js +1 -1
- package/dist/plugin/media.js +1 -1
- package/dist/plugin/osd.js +2 -2
- package/dist/plugin/packages.js +1 -1
- package/dist/plugin/passes.d.ts +111 -0
- package/dist/plugin/passes.js +341 -0
- package/dist/plugin/player.d.ts +2 -3
- package/dist/plugin/player.js +2 -7
- package/dist/plugin/resolvers.js +1 -1
- package/dist/plugin/runtime.d.ts +24 -209
- package/dist/plugin/runtime.js +34 -770
- package/dist/plugin/scope.d.ts +2 -1
- package/dist/plugin/scope.js +5 -2
- package/dist/plugin/services/dialogs.d.ts +3 -0
- package/dist/plugin/services/dialogs.js +62 -0
- package/dist/plugin/services.js +1 -1
- package/dist/plugin/settings.js +2 -2
- package/dist/plugin/shaders.js +2 -1
- package/dist/plugin/stream.js +1 -2
- package/dist/plugin/subtitles.js +2 -2
- package/dist/plugin/testing/transport.js +1 -1
- package/dist/plugin/timeline.js +2 -1
- package/dist/plugin/view/base.d.ts +29 -0
- package/dist/plugin/view/base.js +88 -0
- package/dist/plugin/view/binder.d.ts +22 -0
- package/dist/plugin/view/binder.js +57 -0
- package/dist/plugin/view/image.d.ts +21 -0
- package/dist/plugin/view/image.js +60 -0
- package/dist/plugin/view/layout.d.ts +47 -0
- package/dist/plugin/view/layout.js +171 -0
- package/dist/plugin/view/panel.d.ts +3 -0
- package/dist/plugin/view/panel.js +178 -0
- package/dist/plugin/view/window.d.ts +3 -0
- package/dist/plugin/view/window.js +99 -0
- package/dist/plugin/view/writer.d.ts +58 -0
- package/dist/plugin/view/writer.js +634 -0
- package/dist/plugin/view.js +2 -1
- package/dist/plugin/window.js +1 -1
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/dist/plugin/ui.d.ts +0 -74
- package/dist/plugin/ui.js +0 -1234
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/** Docked panels (`plugin.panels`): one Slint component per position. */
|
|
2
|
+
import { Latest } from "../latest.js";
|
|
3
|
+
import { failure, quiet } from "../errors.js";
|
|
4
|
+
import { dropsOf } from "../drops.js";
|
|
5
|
+
import { exported, ViewBase } from "./base.js";
|
|
6
|
+
import { backdrop, layoutOf, slot } from "./layout.js";
|
|
7
|
+
class PanelHandle extends ViewBase {
|
|
8
|
+
ready;
|
|
9
|
+
#slot;
|
|
10
|
+
#mounted = false;
|
|
11
|
+
#locks = { focused: false, dragging: false, popupOpen: false };
|
|
12
|
+
#interaction;
|
|
13
|
+
constructor(scope, slot, file, options) {
|
|
14
|
+
// One panel per slot (SDK 1.6.15, player 0.7.6; a player before 0.7.6
|
|
15
|
+
// refuses a second panel with `conflict`).
|
|
16
|
+
if (scope.runtime.views.has(`panel:${slot}`))
|
|
17
|
+
throw new Error(`this plugin already has a panel in the ${slot}`);
|
|
18
|
+
super(scope);
|
|
19
|
+
this.#slot = slot;
|
|
20
|
+
const runtime = scope.runtime;
|
|
21
|
+
runtime.views.set(`panel:${slot}`, (callback, args) => this.dispatch(callback, args));
|
|
22
|
+
this.#interaction = new Latest((locks) => runtime.host.ui.interaction(locks, this.#named()));
|
|
23
|
+
const component = options.component ?? exported(runtime.directory, file, "component");
|
|
24
|
+
const global = options.global ?? exported(runtime.directory, file, "global");
|
|
25
|
+
scope.add(this);
|
|
26
|
+
this.ready = runtime
|
|
27
|
+
.starting((async () => {
|
|
28
|
+
// Let handlers and bindings declared right after mount() join in.
|
|
29
|
+
await Promise.resolve();
|
|
30
|
+
const properties = this.wired(this.snapshot());
|
|
31
|
+
await runtime.host.ui.mount({
|
|
32
|
+
slot,
|
|
33
|
+
file,
|
|
34
|
+
component,
|
|
35
|
+
global,
|
|
36
|
+
...(options.title === undefined ? {} : { title: options.title }),
|
|
37
|
+
properties,
|
|
38
|
+
});
|
|
39
|
+
this.#mounted = true;
|
|
40
|
+
this.shown(properties);
|
|
41
|
+
this.flush();
|
|
42
|
+
// The position's state changed: refresh it if this plugin reads it.
|
|
43
|
+
await layoutOf(runtime).refresh();
|
|
44
|
+
})(), (error) => scope.report(error))
|
|
45
|
+
.then(() => { });
|
|
46
|
+
quiet(this.ready);
|
|
47
|
+
}
|
|
48
|
+
live() {
|
|
49
|
+
return this.#mounted && !this.closed;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The slot to name in a request about this panel: only while the plugin
|
|
53
|
+
* has panels in more than one slot (the player then needs it); a plugin
|
|
54
|
+
* with one panel sends what it always sent.
|
|
55
|
+
*/
|
|
56
|
+
#named() {
|
|
57
|
+
const panels = [...this.scope.runtime.views.keys()].filter((key) => key.startsWith("panel:"));
|
|
58
|
+
return panels.length > 1 ? this.#slot : undefined;
|
|
59
|
+
}
|
|
60
|
+
write(property, write) {
|
|
61
|
+
return this.scope.runtime.host.ui.setMount(property, write, this.#named());
|
|
62
|
+
}
|
|
63
|
+
get state() {
|
|
64
|
+
const service = layoutOf(this.scope.runtime);
|
|
65
|
+
service.start();
|
|
66
|
+
return this.live()
|
|
67
|
+
? panelState(service.state, this.#slot, this.scope.runtime.manifest.id)
|
|
68
|
+
: null;
|
|
69
|
+
}
|
|
70
|
+
onChange(listener) {
|
|
71
|
+
const scope = this.scope;
|
|
72
|
+
let last = JSON.stringify(this.state);
|
|
73
|
+
const watch = layoutOf(scope.runtime).watch(() => {
|
|
74
|
+
const state = this.state;
|
|
75
|
+
const next = JSON.stringify(state);
|
|
76
|
+
if (next === last)
|
|
77
|
+
return;
|
|
78
|
+
last = next;
|
|
79
|
+
scope.run(listener, state);
|
|
80
|
+
});
|
|
81
|
+
return scope.add({
|
|
82
|
+
dispose: async () => {
|
|
83
|
+
scope.forget(watch);
|
|
84
|
+
await watch.dispose();
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async show() {
|
|
89
|
+
await this.ready;
|
|
90
|
+
if (this.#slot === "backdrop")
|
|
91
|
+
throw failure("invalid", "the player shows the backdrop by itself");
|
|
92
|
+
await layoutOf(this.scope.runtime).set(this.#slot, {
|
|
93
|
+
active: this.scope.runtime.manifest.id,
|
|
94
|
+
visible: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async toggle() {
|
|
98
|
+
await this.ready;
|
|
99
|
+
if (this.#slot === "backdrop")
|
|
100
|
+
throw failure("invalid", "the player shows the backdrop by itself");
|
|
101
|
+
await layoutOf(this.scope.runtime).read();
|
|
102
|
+
const state = this.state;
|
|
103
|
+
// A switch, the same in every mode (user ruling 2026-11-08): off when
|
|
104
|
+
// on and current, else on at this tab. `reveal: false`: in fullscreen
|
|
105
|
+
// nothing pops out, the right edge reveals it (player 0.7.6; older
|
|
106
|
+
// players ignore it and pop it out).
|
|
107
|
+
if (state?.preferred && state.active)
|
|
108
|
+
await layoutOf(this.scope.runtime).set(this.#slot, { visible: false });
|
|
109
|
+
else
|
|
110
|
+
await layoutOf(this.scope.runtime).set(this.#slot, {
|
|
111
|
+
active: this.scope.runtime.manifest.id,
|
|
112
|
+
visible: true,
|
|
113
|
+
reveal: false,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async showMenu(items, at) {
|
|
117
|
+
await this.ready;
|
|
118
|
+
await this.scope.runtime.host.ui.menu(items.map((item) => item?.id ?? null), at.x, at.y, this.#named());
|
|
119
|
+
}
|
|
120
|
+
onDrop(listener) {
|
|
121
|
+
const watch = dropsOf(this.scope.runtime).listen(this.scope, (event) => this.scope.run(listener, event));
|
|
122
|
+
return this.scope.add({ dispose: () => watch.dispose() });
|
|
123
|
+
}
|
|
124
|
+
setInteraction(locks) {
|
|
125
|
+
Object.assign(this.#locks, locks);
|
|
126
|
+
this.#interaction
|
|
127
|
+
.push({ ...this.#locks })
|
|
128
|
+
.catch((error) => this.scope.report(error));
|
|
129
|
+
}
|
|
130
|
+
async dispose() {
|
|
131
|
+
if (this.closed)
|
|
132
|
+
return;
|
|
133
|
+
this.scope.forget(this);
|
|
134
|
+
this.#interaction.stop();
|
|
135
|
+
if (this.#mounted)
|
|
136
|
+
await this.scope.runtime.host.ui.unmount(this.#named()).catch(() => { });
|
|
137
|
+
this.#mounted = false;
|
|
138
|
+
await this.release();
|
|
139
|
+
this.scope.runtime.views.delete(`panel:${this.#slot}`);
|
|
140
|
+
// Listeners of `onChange` hear `null`.
|
|
141
|
+
this.scope.runtime.invalidate();
|
|
142
|
+
await layoutOf(this.scope.runtime).refresh();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** One panel's view of its position (`Panel.state`). */
|
|
146
|
+
function panelState(layout, slot, plugin) {
|
|
147
|
+
if (slot === "backdrop")
|
|
148
|
+
return (layout.backdrop && {
|
|
149
|
+
active: true,
|
|
150
|
+
preferred: true,
|
|
151
|
+
effective: layout.backdrop.effective,
|
|
152
|
+
reason: null,
|
|
153
|
+
detached: false,
|
|
154
|
+
});
|
|
155
|
+
const state = layout[slot];
|
|
156
|
+
if (!state)
|
|
157
|
+
return null;
|
|
158
|
+
// Players without tabs (no `panels`): the position holds this panel only.
|
|
159
|
+
const panels = layout.tabs[slot];
|
|
160
|
+
const own = panels?.find((panel) => panel.plugin === plugin);
|
|
161
|
+
if (panels && !own)
|
|
162
|
+
return null;
|
|
163
|
+
const active = own?.active ?? true;
|
|
164
|
+
return {
|
|
165
|
+
active,
|
|
166
|
+
preferred: state.preferred,
|
|
167
|
+
effective: state.effective && active,
|
|
168
|
+
reason: active ? state.reason : "otherTab",
|
|
169
|
+
detached: state.detached,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export const panels = (scope) => ({
|
|
173
|
+
mount: (position, file, options = {}) => new PanelHandle(scope, position, file, options),
|
|
174
|
+
bottom: slot(scope, "bottom"),
|
|
175
|
+
sidebar: slot(scope, "sidebar"),
|
|
176
|
+
adjust: slot(scope, "adjust"),
|
|
177
|
+
backdrop: backdrop(scope),
|
|
178
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** Tool windows (`plugin.windows`): one Slint window per handle. */
|
|
2
|
+
import { basename, extname } from "node:path";
|
|
3
|
+
import { failure } from "../errors.js";
|
|
4
|
+
import { ViewBase } from "./base.js";
|
|
5
|
+
import { kebab } from "./writer.js";
|
|
6
|
+
class WindowHandle extends ViewBase {
|
|
7
|
+
#id;
|
|
8
|
+
#file;
|
|
9
|
+
#opened = false;
|
|
10
|
+
#queue = Promise.resolve();
|
|
11
|
+
constructor(scope, file, id) {
|
|
12
|
+
if (scope.runtime.views.has(id))
|
|
13
|
+
throw failure("conflict", `window ${id} already exists; pass a distinct id`);
|
|
14
|
+
super(scope);
|
|
15
|
+
this.#file = file;
|
|
16
|
+
this.#id = id;
|
|
17
|
+
scope.runtime.views.set(id, (callback, args) => this.dispatch(callback, args));
|
|
18
|
+
// The user closed it with the title bar: the host already dropped it.
|
|
19
|
+
this.#closedByUser = scope.runtime.raw("ui.closed", (data) => {
|
|
20
|
+
if (data.view !== this.#id || !this.#opened)
|
|
21
|
+
return;
|
|
22
|
+
this.#opened = false;
|
|
23
|
+
for (const listener of [...this.#closeListeners])
|
|
24
|
+
this.scope.run(listener);
|
|
25
|
+
this.scope.update();
|
|
26
|
+
});
|
|
27
|
+
scope.add(this);
|
|
28
|
+
}
|
|
29
|
+
#closeListeners = new Set();
|
|
30
|
+
#closedByUser;
|
|
31
|
+
onClose(listener) {
|
|
32
|
+
this.#closeListeners.add(listener);
|
|
33
|
+
return this.scope.onDispose(() => {
|
|
34
|
+
this.#closeListeners.delete(listener);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
get opened() {
|
|
38
|
+
return this.#opened;
|
|
39
|
+
}
|
|
40
|
+
live() {
|
|
41
|
+
return this.#opened && !this.closed;
|
|
42
|
+
}
|
|
43
|
+
write(property, write) {
|
|
44
|
+
return this.scope.runtime.host.ui.set(this.#id, property, write);
|
|
45
|
+
}
|
|
46
|
+
on(callback, handler) {
|
|
47
|
+
// The host forwards only callbacks named when the window opens.
|
|
48
|
+
if (this.#opened && !this.handlers.has(kebab(callback)))
|
|
49
|
+
throw new Error(`bind window handler ${callback} before the first show()`);
|
|
50
|
+
return super.on(callback, handler);
|
|
51
|
+
}
|
|
52
|
+
/** Window RPCs run strictly in order; a failure does not block later calls. */
|
|
53
|
+
#serial(task) {
|
|
54
|
+
const next = this.#queue.then(task, task);
|
|
55
|
+
this.#queue = next.catch(() => { });
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
show() {
|
|
59
|
+
return this.#serial(async () => {
|
|
60
|
+
if (this.closed)
|
|
61
|
+
return;
|
|
62
|
+
const ui = this.scope.runtime.host.ui;
|
|
63
|
+
if (this.#opened)
|
|
64
|
+
return ui.show(this.#id, true);
|
|
65
|
+
const initial = this.wired(this.snapshot(), true);
|
|
66
|
+
await ui.open(this.#id, this.#file, initial, [...this.handlers.keys()]);
|
|
67
|
+
this.#opened = true;
|
|
68
|
+
this.shown(initial);
|
|
69
|
+
this.flush();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
hide() {
|
|
73
|
+
return this.#serial(async () => {
|
|
74
|
+
if (this.#opened)
|
|
75
|
+
await this.scope.runtime.host.ui.show(this.#id, false);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
close() {
|
|
79
|
+
return this.#serial(async () => {
|
|
80
|
+
if (!this.#opened)
|
|
81
|
+
return;
|
|
82
|
+
this.#opened = false;
|
|
83
|
+
await this.scope.runtime.host.ui.close(this.#id);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async dispose() {
|
|
87
|
+
if (this.closed)
|
|
88
|
+
return;
|
|
89
|
+
this.scope.forget(this);
|
|
90
|
+
await this.close().catch(() => { });
|
|
91
|
+
await this.release();
|
|
92
|
+
await this.#closedByUser.dispose();
|
|
93
|
+
this.#closeListeners.clear();
|
|
94
|
+
this.scope.runtime.views.delete(this.#id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export const windows = (scope) => ({
|
|
98
|
+
create: (file, options) => new WindowHandle(scope, file, options?.id ?? basename(file, extname(file))),
|
|
99
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Json, PropertyWrite } from "../../wire.ts";
|
|
2
|
+
import type { PropValue, Props } from "../types.ts";
|
|
3
|
+
/** Slint names are kebab-case; plugin code may use camelCase. */
|
|
4
|
+
export declare const kebab: (name: string) => string;
|
|
5
|
+
export declare const encode: (props: Props) => {
|
|
6
|
+
[k: string]: PropValue;
|
|
7
|
+
};
|
|
8
|
+
/** Where a {@link ViewWriter}'s writes go, and what it asks of its view. */
|
|
9
|
+
export interface WriterTarget {
|
|
10
|
+
/** The view shows its properties: writes may be sent. */
|
|
11
|
+
live(): boolean;
|
|
12
|
+
/** Send one property write to the player. */
|
|
13
|
+
write(property: string, write: PropertyWrite): Promise<void>;
|
|
14
|
+
/** Report a failure (the plugin's error handler and the player's log). */
|
|
15
|
+
report(error: unknown): void;
|
|
16
|
+
/** The player takes row patches (and `seq` on whole writes). */
|
|
17
|
+
patches(): boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* One view's properties: their values now, what the player shows as far as
|
|
21
|
+
* this side knows, and the writes that bring the one to the other.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ViewWriter {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(target: WriterTarget);
|
|
26
|
+
/**
|
|
27
|
+
* Initial properties in wire form. A released image is left out (the
|
|
28
|
+
* property keeps its default) and reported, like a failed write. With a
|
|
29
|
+
* player that takes row patches, the arrays of one request carry at most
|
|
30
|
+
* {@link CHUNK_BYTES} of rows together (SDK 1.5.46): an array over that
|
|
31
|
+
* goes with its first rows, and the first flush sends the rest as patches.
|
|
32
|
+
* An array over the value limits is refused here, as the player would
|
|
33
|
+
* refuse it whole: left out and reported (a panel, a settings page), or,
|
|
34
|
+
* with `refuse` (a window's `show`), thrown.
|
|
35
|
+
*/
|
|
36
|
+
wired(values: Record<string, PropValue>, refuse?: boolean): Record<string, Json>;
|
|
37
|
+
/** `View.set`: each value is sent once even if equal to what was sent. */
|
|
38
|
+
set(props: Props): void;
|
|
39
|
+
/** A bound value (Slint name): sent by the next flush if it changed. */
|
|
40
|
+
assign(key: string, value: PropValue): void;
|
|
41
|
+
/** Every property's value now, by Slint name. */
|
|
42
|
+
values(): Record<string, PropValue>;
|
|
43
|
+
/** The host already shows these values (e.g. sent as initial properties). */
|
|
44
|
+
shown(values: Record<string, Json>): void;
|
|
45
|
+
/**
|
|
46
|
+
* Send what changed. A bound value is sent when it differs from the last one
|
|
47
|
+
* sent; a `set()` value is always sent once. Nothing is resent merely
|
|
48
|
+
* because a callback arrived, so text the user typed into an in-out
|
|
49
|
+
* property survives other callbacks (call `set()` to pull a control back).
|
|
50
|
+
* A failed bound value is not repeated until it changes; an explicit `set()`
|
|
51
|
+
* is, after a back-off (0.25 s doubling to 30 s). Only the first failure of
|
|
52
|
+
* a failing streak is reported, so an error handler that writes a status to
|
|
53
|
+
* a broken view cannot spin.
|
|
54
|
+
*/
|
|
55
|
+
flush(): void;
|
|
56
|
+
/** The view closed: no retry after a back-off. */
|
|
57
|
+
stop(): void;
|
|
58
|
+
}
|