@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.
@@ -8,7 +8,7 @@ export type ResolvedResource = {
8
8
  headers?: Record<string, string>;
9
9
  };
10
10
  /**
11
- * What a resolver returns ({@link ResolverOptions.resolve}): the playable
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 has one owner.
46
- * Resolution shares source.submit arbitration and the original operation ID.
47
- * This backend supports paired streams with identical header maps; different
48
- * maps fail explicitly. No header is silently discarded or made global. */
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
  };
@@ -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 has one owner.
4
- * Resolution shares source.submit arbitration and the original operation ID.
5
- * This backend supports paired streams with identical header maps; different
6
- * maps fail explicitly. No header is silently discarded or made global. */
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
- const result = await handler({
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 (!controller.signal.aborted && !disposed) {
31
+ if (live())
31
32
  await transport
32
33
  .request("sourceResolvers.respond", {
33
34
  requestId: request.requestId,
34
- error: String(error).slice(0, 2048),
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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
- for block in root.blocks: MarkdownBlockView {
206
- block: block;
207
- available: scroll.visible-width - 2 * root.inset;
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
  }