@vestra-player/sdk 1.5.2 → 1.5.4
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/dist/plugin/resolvers.js +16 -4
- package/dist/plugin/stream.d.ts +11 -1
- package/dist/plugin/stream.js +142 -68
- package/dist/plugin/testing.d.ts +48 -2
- package/dist/plugin/testing.js +218 -16
- package/dist/plugin/types.d.ts +176 -27
- package/dist/plugin/ui.js +22 -7
- package/dist/source-resolvers.d.ts +33 -6
- package/dist/source-resolvers.js +42 -14
- package/dist/wire.d.ts +1 -1
- package/package.json +1 -1
- package/slint/vestra/markdown.slint +56 -5
|
@@ -8,7 +8,7 @@ export type ResolvedResource = {
|
|
|
8
8
|
headers?: Record<string, string>;
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
|
-
* What a resolver returns ({@link
|
|
11
|
+
* What a resolver returns ({@link WebResolverOptions.resolve}): the playable
|
|
12
12
|
* stream(s) behind a page URL. Both streams must use identical `headers`;
|
|
13
13
|
* different maps fail explicitly (nothing is dropped silently).
|
|
14
14
|
*
|
|
@@ -35,21 +35,48 @@ export type ResolvedMedia = {
|
|
|
35
35
|
/** A separate audio stream, when audio and video are split. */
|
|
36
36
|
audio?: ResolvedResource;
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* A resolver declaration on the wire: a web resolver (`schemes` + `hosts`)
|
|
40
|
+
* or, from player 0.4.1, a file resolver (`extensions`), never both.
|
|
41
|
+
*/
|
|
38
42
|
export type SourceResolverDeclaration = {
|
|
39
43
|
id: string;
|
|
40
44
|
/** Finite initial contract: http and/or https, exact normalized host names. */
|
|
41
45
|
schemes: ("http" | "https")[];
|
|
42
46
|
hosts: string[];
|
|
47
|
+
} | {
|
|
48
|
+
id: string;
|
|
49
|
+
/** Local files by extension: 1–16, lower-case letters or digits, no dot. */
|
|
50
|
+
extensions: string[];
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* A file resolver's answer as the handler gives it: the producer it prepared
|
|
54
|
+
* (sent to the host), `accepted` once the host selected it (with the stamp of
|
|
55
|
+
* the source it became) and `abandoned` when it never will be: the request
|
|
56
|
+
* was cancelled or answered too late. Exactly one of the two is called.
|
|
57
|
+
*/
|
|
58
|
+
export type ResolvedProducer = {
|
|
59
|
+
kind: "stream";
|
|
60
|
+
producerId: number;
|
|
61
|
+
accepted(reply: {
|
|
62
|
+
stamp: {
|
|
63
|
+
generation: number;
|
|
64
|
+
intentVersion: number;
|
|
65
|
+
};
|
|
66
|
+
}): Promise<void>;
|
|
67
|
+
abandoned(): Promise<void>;
|
|
43
68
|
};
|
|
44
69
|
export declare function sourceResolverClient(transport: Transport): {
|
|
45
|
-
/** Requires source.resolve. One registered host/scheme pair
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
70
|
+
/** Requires source.resolve. One registered host/scheme pair, and one file
|
|
71
|
+
* extension, has one owner (another plugin: `conflict`, reason
|
|
72
|
+
* `resolver-taken`). Resolution shares source.submit arbitration and the
|
|
73
|
+
* original operation ID. This backend supports paired streams with
|
|
74
|
+
* identical header maps; different maps fail explicitly. No header is
|
|
75
|
+
* silently discarded or made global. A failure is sent as its message. */
|
|
49
76
|
register(declaration: SourceResolverDeclaration, handler: (request: {
|
|
50
77
|
url: string;
|
|
51
78
|
signal: AbortSignal;
|
|
52
|
-
}) => Promise<ResolvedMedia>): Promise<{
|
|
79
|
+
}) => Promise<ResolvedMedia | ResolvedProducer>): Promise<{
|
|
53
80
|
dispose(): Promise<void>;
|
|
54
81
|
}>;
|
|
55
82
|
};
|
package/dist/source-resolvers.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
const isProducer = (result) => result?.kind === "stream";
|
|
2
|
+
/** The words of a failure, as the user will read them. */
|
|
3
|
+
const reason = (error) => (error instanceof Error ? error.message : String(error)).slice(0, 2048);
|
|
1
4
|
export function sourceResolverClient(transport) {
|
|
2
5
|
return {
|
|
3
|
-
/** Requires source.resolve. One registered host/scheme pair
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
/** Requires source.resolve. One registered host/scheme pair, and one file
|
|
7
|
+
* extension, has one owner (another plugin: `conflict`, reason
|
|
8
|
+
* `resolver-taken`). Resolution shares source.submit arbitration and the
|
|
9
|
+
* original operation ID. This backend supports paired streams with
|
|
10
|
+
* identical header maps; different maps fail explicitly. No header is
|
|
11
|
+
* silently discarded or made global. A failure is sent as its message. */
|
|
7
12
|
async register(declaration, handler) {
|
|
8
13
|
const active = new Map();
|
|
9
14
|
let disposed = false;
|
|
@@ -13,33 +18,56 @@ export function sourceResolverClient(transport) {
|
|
|
13
18
|
return;
|
|
14
19
|
const controller = new AbortController();
|
|
15
20
|
active.set(request.requestId, controller);
|
|
21
|
+
const live = () => !controller.signal.aborted && !disposed;
|
|
16
22
|
void (async () => {
|
|
23
|
+
let result;
|
|
17
24
|
try {
|
|
18
|
-
|
|
25
|
+
result = await handler({
|
|
19
26
|
url: request.url,
|
|
20
27
|
signal: controller.signal,
|
|
21
28
|
});
|
|
22
|
-
if (!controller.signal.aborted && !disposed) {
|
|
23
|
-
await transport.request("sourceResolvers.respond", {
|
|
24
|
-
requestId: request.requestId,
|
|
25
|
-
result,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
29
|
}
|
|
29
30
|
catch (error) {
|
|
30
|
-
if (
|
|
31
|
+
if (live())
|
|
31
32
|
await transport
|
|
32
33
|
.request("sourceResolvers.respond", {
|
|
33
34
|
requestId: request.requestId,
|
|
34
|
-
error:
|
|
35
|
+
error: reason(error),
|
|
35
36
|
})
|
|
36
37
|
.catch(() => { });
|
|
38
|
+
active.delete(request.requestId);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
if (!isProducer(result)) {
|
|
43
|
+
if (live())
|
|
44
|
+
await transport.request("sourceResolvers.respond", {
|
|
45
|
+
requestId: request.requestId,
|
|
46
|
+
result,
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!live())
|
|
51
|
+
return void (await result.abandoned());
|
|
52
|
+
let reply;
|
|
53
|
+
try {
|
|
54
|
+
// Answered: the host no longer cancels this request.
|
|
55
|
+
active.delete(request.requestId);
|
|
56
|
+
reply = await transport.request("sourceResolvers.respond", {
|
|
57
|
+
requestId: request.requestId,
|
|
58
|
+
result: { kind: "stream", producerId: result.producerId },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
await result.abandoned();
|
|
63
|
+
throw error;
|
|
37
64
|
}
|
|
65
|
+
await result.accepted(reply);
|
|
38
66
|
}
|
|
39
67
|
finally {
|
|
40
68
|
active.delete(request.requestId);
|
|
41
69
|
}
|
|
42
|
-
})();
|
|
70
|
+
})().catch(() => { });
|
|
43
71
|
});
|
|
44
72
|
const offCancel = transport.on("source.resolveCancel", (value) => {
|
|
45
73
|
const request = value;
|
package/dist/wire.d.ts
CHANGED
|
@@ -593,7 +593,7 @@ export declare function createWire(transport: Transport): {
|
|
|
593
593
|
register(declaration: import("./source-resolvers.ts").SourceResolverDeclaration, handler: (request: {
|
|
594
594
|
url: string;
|
|
595
595
|
signal: AbortSignal;
|
|
596
|
-
}) => Promise<import("./source-resolvers.ts").ResolvedMedia>): Promise<{
|
|
596
|
+
}) => Promise<import("./source-resolvers.ts").ResolvedMedia | import("./source-resolvers.ts").ResolvedProducer>): Promise<{
|
|
597
597
|
dispose(): Promise<void>;
|
|
598
598
|
}>;
|
|
599
599
|
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
// Markdown display (store proposal §5):
|
|
2
|
-
// import { MarkdownView, MarkdownBlock } from "@vestra/markdown.slint";
|
|
2
|
+
// import { MarkdownView, MarkdownBlocks, MarkdownBlock } from "@vestra/markdown.slint";
|
|
3
3
|
// Give `blocks` the value of `plugin.ui.markdown(text)`; the player parses
|
|
4
4
|
// the Markdown and fills the rows. Links report `link-clicked(url)` with the
|
|
5
5
|
// address resolved against the document's base; the plugin decides what to
|
|
6
6
|
// open (usually `plugin.system.openUrl`).
|
|
7
|
+
// `MarkdownView` scrolls one document. `MarkdownBlocks` does not scroll and
|
|
8
|
+
// is as tall as its content: put it in your own layout or in a `ListView`
|
|
9
|
+
// row (a chat message: a `[MarkdownBlock]` field of the row's struct).
|
|
10
|
+
// `code-copy: true` adds a 复制 button to code blocks that reports
|
|
11
|
+
// `copy-code(code)`; the plugin copies it (`plugin.clipboard`).
|
|
7
12
|
import { ScrollView } from "std-widgets.slint";
|
|
8
13
|
import { Theme } from "theme.slint";
|
|
9
14
|
import { Icon } from "icons.slint";
|
|
@@ -45,7 +50,9 @@ export struct MarkdownBlock {
|
|
|
45
50
|
component MarkdownBlockView {
|
|
46
51
|
in property <MarkdownBlock> block;
|
|
47
52
|
in property <length> available;
|
|
53
|
+
in property <bool> code-copy;
|
|
48
54
|
callback link-clicked(string);
|
|
55
|
+
callback copy-code(string);
|
|
49
56
|
private property <length> quote-indent: block.quote * 12px;
|
|
50
57
|
private property <length> list-indent: block.indent * 16px;
|
|
51
58
|
private property <length> inner: max(16px, root.available - root.quote-indent - root.list-indent);
|
|
@@ -124,6 +131,24 @@ component MarkdownBlockView {
|
|
|
124
131
|
default-font-size: Theme.font-body;
|
|
125
132
|
}
|
|
126
133
|
}
|
|
134
|
+
if root.code-copy: Rectangle {
|
|
135
|
+
x: parent.width - self.width - 4px;
|
|
136
|
+
y: 4px;
|
|
137
|
+
width: copy-label.preferred-width + 12px;
|
|
138
|
+
height: Theme.button-small-height;
|
|
139
|
+
border-radius: Theme.radius-control;
|
|
140
|
+
background: copy-touch.pressed ? Theme.button-press : copy-touch.has-hover ? Theme.button-hover : Theme.button-bg;
|
|
141
|
+
copy-label := Text {
|
|
142
|
+
text: "复制";
|
|
143
|
+
color: copy-touch.has-hover ? Theme.text : Theme.text-dim;
|
|
144
|
+
font-size: Theme.font-body;
|
|
145
|
+
vertical-alignment: center;
|
|
146
|
+
}
|
|
147
|
+
copy-touch := TouchArea {
|
|
148
|
+
mouse-cursor: pointer;
|
|
149
|
+
clicked => { root.copy-code(block.code); }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
127
152
|
}
|
|
128
153
|
if block.kind == "image" && block.has-image: HorizontalLayout {
|
|
129
154
|
alignment: start;
|
|
@@ -186,12 +211,38 @@ component MarkdownBlockView {
|
|
|
186
211
|
}
|
|
187
212
|
}
|
|
188
213
|
|
|
214
|
+
// Markdown blocks without scrolling, as tall as their content.
|
|
215
|
+
export component MarkdownBlocks inherits Rectangle {
|
|
216
|
+
in property <[MarkdownBlock]> blocks;
|
|
217
|
+
// A 复制 button on code blocks, reporting `copy-code(code)`.
|
|
218
|
+
in property <bool> code-copy: false;
|
|
219
|
+
callback link-clicked(string);
|
|
220
|
+
callback copy-code(string);
|
|
221
|
+
min-height: column.min-height;
|
|
222
|
+
preferred-height: column.preferred-height;
|
|
223
|
+
column := VerticalLayout {
|
|
224
|
+
width: root.width;
|
|
225
|
+
spacing: 8px;
|
|
226
|
+
alignment: start;
|
|
227
|
+
for block in root.blocks: MarkdownBlockView {
|
|
228
|
+
block: block;
|
|
229
|
+
available: root.width;
|
|
230
|
+
code-copy: root.code-copy;
|
|
231
|
+
link-clicked(url) => { root.link-clicked(url); }
|
|
232
|
+
copy-code(code) => { root.copy-code(code); }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
189
237
|
// A scrolling Markdown document.
|
|
190
238
|
export component MarkdownView inherits Rectangle {
|
|
191
239
|
in property <[MarkdownBlock]> blocks;
|
|
192
240
|
// Space around the document.
|
|
193
241
|
in property <length> inset: 10px;
|
|
242
|
+
// A 复制 button on code blocks, reporting `copy-code(code)`.
|
|
243
|
+
in property <bool> code-copy: false;
|
|
194
244
|
callback link-clicked(string);
|
|
245
|
+
callback copy-code(string);
|
|
195
246
|
scroll := ScrollView {
|
|
196
247
|
width: 100%;
|
|
197
248
|
height: 100%;
|
|
@@ -200,12 +251,12 @@ export component MarkdownView inherits Rectangle {
|
|
|
200
251
|
column := VerticalLayout {
|
|
201
252
|
width: scroll.visible-width;
|
|
202
253
|
padding: root.inset;
|
|
203
|
-
spacing: 8px;
|
|
204
254
|
alignment: start;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
255
|
+
MarkdownBlocks {
|
|
256
|
+
blocks: root.blocks;
|
|
257
|
+
code-copy: root.code-copy;
|
|
208
258
|
link-clicked(url) => { root.link-clicked(url); }
|
|
259
|
+
copy-code(code) => { root.copy-code(code); }
|
|
209
260
|
}
|
|
210
261
|
}
|
|
211
262
|
}
|