@vestra-player/sdk 1.5.1 → 1.5.3
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 +30 -0
- package/dist/plugin/packages.js +5 -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 +88 -2
- package/dist/plugin/testing.js +360 -22
- package/dist/plugin/types.d.ts +369 -22
- package/dist/plugin/ui.js +99 -3
- package/dist/source-resolvers.d.ts +33 -6
- package/dist/source-resolvers.js +42 -14
- package/dist/wire.d.ts +20 -1
- package/package.json +1 -1
package/dist/plugin/types.d.ts
CHANGED
|
@@ -1212,6 +1212,46 @@ export interface SlotState {
|
|
|
1212
1212
|
/** The sidebar floats in its own window; always `false` for the bottom bar. */
|
|
1213
1213
|
detached: boolean;
|
|
1214
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* One panel of a position ({@link PanelSlot.panels}): a tab of the sidebar.
|
|
1217
|
+
*
|
|
1218
|
+
* @since 1.5.2
|
|
1219
|
+
* @category UI
|
|
1220
|
+
*/
|
|
1221
|
+
export interface SlotPanel {
|
|
1222
|
+
/** The manifest id of the plugin that mounted it (pass it to {@link PanelSlot.activate}). */
|
|
1223
|
+
plugin: string;
|
|
1224
|
+
/** Its tab label: the `title` of {@link Panels.mount}, else the plugin's name. */
|
|
1225
|
+
title: string;
|
|
1226
|
+
/** Its tab is the current one (the panel the position shows). */
|
|
1227
|
+
active: boolean;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* The state of one panel ({@link Panel.state}): its position's state, seen
|
|
1231
|
+
* from this panel, which may be one of several sidebar tabs.
|
|
1232
|
+
*
|
|
1233
|
+
* @since 1.5.2
|
|
1234
|
+
* @category UI
|
|
1235
|
+
*/
|
|
1236
|
+
export interface PanelState {
|
|
1237
|
+
/**
|
|
1238
|
+
* Its tab is the current one. Always `true` for the bottom bar, the
|
|
1239
|
+
* backdrop and a sidebar panel without other tabs.
|
|
1240
|
+
*/
|
|
1241
|
+
active: boolean;
|
|
1242
|
+
/** The position's preference (the user's choice to show it). */
|
|
1243
|
+
preferred: boolean;
|
|
1244
|
+
/** This panel is on screen now: the position is, and this is its current tab. */
|
|
1245
|
+
effective: boolean;
|
|
1246
|
+
/**
|
|
1247
|
+
* Why this panel is not on screen, `null` while it is: `otherTab` while
|
|
1248
|
+
* another plugin's tab is the current one (whatever the position's state),
|
|
1249
|
+
* else the position's reason ({@link SlotState.reason}).
|
|
1250
|
+
*/
|
|
1251
|
+
reason: SlotState["reason"] | "otherTab";
|
|
1252
|
+
/** The sidebar floats in its own window (with all its tabs). */
|
|
1253
|
+
detached: boolean;
|
|
1254
|
+
}
|
|
1215
1255
|
/**
|
|
1216
1256
|
* The bottom bar or the sidebar position (`plugin.panels.bottom`,
|
|
1217
1257
|
* `plugin.panels.sidebar`), independent of who mounted the panel there.
|
|
@@ -1246,15 +1286,31 @@ export interface SlotState {
|
|
|
1246
1286
|
* @category UI
|
|
1247
1287
|
*/
|
|
1248
1288
|
export interface PanelSlot {
|
|
1249
|
-
/**
|
|
1289
|
+
/**
|
|
1290
|
+
* The current state; `null` while no plugin has a panel there (or without
|
|
1291
|
+
* `ui.mount`). With sidebar tabs it is the whole position's: shown,
|
|
1292
|
+
* hidden and floating apply to all tabs, which {@link PanelSlot.panels}
|
|
1293
|
+
* lists.
|
|
1294
|
+
*/
|
|
1250
1295
|
readonly state: SlotState | null;
|
|
1296
|
+
/**
|
|
1297
|
+
* The position's panels in tab order: the sidebar's tabs, the bottom
|
|
1298
|
+
* bar's one panel; `null` while there is none (as `state`), and from
|
|
1299
|
+
* players without sidebar tabs (0.4.0 and older). Refreshed with `state`.
|
|
1300
|
+
*
|
|
1301
|
+
* @throws Never.
|
|
1302
|
+
*
|
|
1303
|
+
* @since 1.5.2
|
|
1304
|
+
*/
|
|
1305
|
+
readonly panels: readonly SlotPanel[] | null;
|
|
1251
1306
|
/**
|
|
1252
1307
|
* Run code when the position's state changes.
|
|
1253
1308
|
*
|
|
1254
1309
|
* @remarks
|
|
1255
1310
|
* Needs `events`: without it the listener runs only after this plugin's own
|
|
1256
|
-
* changes. Called with `null` when the panel there is unmounted
|
|
1257
|
-
*
|
|
1311
|
+
* changes. Called with `null` when the panel there is unmounted, and also
|
|
1312
|
+
* when only the tabs changed ({@link PanelSlot.panels}: a panel added or
|
|
1313
|
+
* removed, another tab current). Errors go to {@link Scope.onError}.
|
|
1258
1314
|
*
|
|
1259
1315
|
* @capability ui.mount and events
|
|
1260
1316
|
*
|
|
@@ -1325,6 +1381,41 @@ export interface PanelSlot {
|
|
|
1325
1381
|
* @since 1.0.0
|
|
1326
1382
|
*/
|
|
1327
1383
|
toggle(): Promise<void>;
|
|
1384
|
+
/**
|
|
1385
|
+
* Make a panel's tab the current one, without showing or hiding the
|
|
1386
|
+
* position.
|
|
1387
|
+
*
|
|
1388
|
+
* @remarks
|
|
1389
|
+
* The sidebar holds the panels of several plugins as tabs (see
|
|
1390
|
+
* {@link Panels.mount}); this switches which one it shows, like a click on
|
|
1391
|
+
* its tab. To show this plugin's own panel, use {@link Panel.show}. The
|
|
1392
|
+
* bottom bar has one panel: activating it changes nothing. Players
|
|
1393
|
+
* without sidebar tabs (0.4.0 and older) ignore it.
|
|
1394
|
+
*
|
|
1395
|
+
* @capability ui.mount
|
|
1396
|
+
*
|
|
1397
|
+
* @param plugin - The manifest id of the plugin whose panel to show, one
|
|
1398
|
+
* of {@link PanelSlot.panels}.
|
|
1399
|
+
* @returns Resolves once the layout has the new current tab, with `state`
|
|
1400
|
+
* refreshed.
|
|
1401
|
+
*
|
|
1402
|
+
* @throws `missing` (rejects) No panel is mounted at this position, or
|
|
1403
|
+
* none of `plugin`.
|
|
1404
|
+
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1405
|
+
* @throws `capability` (rejects) The manifest does not declare `ui.mount`.
|
|
1406
|
+
*
|
|
1407
|
+
* @example A command that shows the playlist's tab
|
|
1408
|
+
* ```ts
|
|
1409
|
+
* plugin.commands.add({
|
|
1410
|
+
* id: "to-playlist",
|
|
1411
|
+
* title: "Show the playlist tab",
|
|
1412
|
+
* run: () => plugin.panels.sidebar.activate("vestra.playlist"),
|
|
1413
|
+
* });
|
|
1414
|
+
* ```
|
|
1415
|
+
*
|
|
1416
|
+
* @since 1.5.2
|
|
1417
|
+
*/
|
|
1418
|
+
activate(plugin: string): Promise<void>;
|
|
1328
1419
|
}
|
|
1329
1420
|
/**
|
|
1330
1421
|
* The sidebar position: a {@link PanelSlot} that can also float in a window
|
|
@@ -1418,14 +1509,25 @@ export interface Panel extends View, Disposable {
|
|
|
1418
1509
|
*
|
|
1419
1510
|
* @remarks
|
|
1420
1511
|
* Mounts made while `activate` runs must succeed before the plugin is
|
|
1421
|
-
* ready: a rejection fails activation
|
|
1422
|
-
*
|
|
1512
|
+
* ready: a rejection fails activation, except `conflict` for a taken
|
|
1513
|
+
* position (below), which the player (0.4.x after 0.4.0) and `TestHost`
|
|
1514
|
+
* do not count: the plugin runs without its panel, its commands and keys
|
|
1515
|
+
* work, and the player shows the refusal (an OSD message,
|
|
1516
|
+
* {@link PluginInfo.warnings} and the text in {@link PluginInfo.error}).
|
|
1517
|
+
* Later failures also go to {@link Scope.onError}.
|
|
1518
|
+
*
|
|
1519
|
+
* A second sidebar panel is not refused: it resolves as another tab
|
|
1520
|
+
* (see {@link Panel.state}). Built-in plugins mount first at start-up,
|
|
1521
|
+
* so the playlist's tab is first and a built-in panel keeps its position.
|
|
1423
1522
|
*
|
|
1424
1523
|
* @throws `ui-prepare` (rejects) The Slint file does not compile, or a
|
|
1425
1524
|
* bound property is not declared on the bridge global; `message` has the
|
|
1426
|
-
* diagnostics.
|
|
1427
|
-
*
|
|
1428
|
-
* @throws `conflict` (rejects) Another plugin
|
|
1525
|
+
* diagnostics. (Players 0.4.0 and older also answer a taken bottom bar
|
|
1526
|
+
* or sidebar with it.)
|
|
1527
|
+
* @throws `conflict` (rejects) Another plugin's panel holds the bottom bar
|
|
1528
|
+
* or the backdrop (shown, or being mounted), or the sidebar has all the
|
|
1529
|
+
* tabs it takes (8); `message` names the position and the holder
|
|
1530
|
+
* (「侧栏已被 vestra.playlist 使用」). Not an activation failure.
|
|
1429
1531
|
* @throws `busy` (rejects) The player is still preparing this plugin's
|
|
1430
1532
|
* previous mount, or its queue of mounts is full.
|
|
1431
1533
|
* @throws `invalid` (rejects) `title` is longer than 128 bytes or has
|
|
@@ -1436,6 +1538,107 @@ export interface Panel extends View, Disposable {
|
|
|
1436
1538
|
* @throws `capability` (rejects) The manifest does not declare `ui.mount`.
|
|
1437
1539
|
*/
|
|
1438
1540
|
readonly ready: Promise<void>;
|
|
1541
|
+
/**
|
|
1542
|
+
* This panel's state: its position's, and whether its tab is the current
|
|
1543
|
+
* one; `null` until {@link Panel.ready} resolved (and after `dispose`).
|
|
1544
|
+
*
|
|
1545
|
+
* @remarks
|
|
1546
|
+
* Read when first used; with `events` it then follows every change (the
|
|
1547
|
+
* user switching tabs, other plugins, the window size), without `events`
|
|
1548
|
+
* only this plugin's own changes. A sidebar panel in a background tab
|
|
1549
|
+
* has `active: false`, `effective: false` and `reason: "otherTab"`.
|
|
1550
|
+
*
|
|
1551
|
+
* @throws Never; `null` stands for "not mounted".
|
|
1552
|
+
*
|
|
1553
|
+
* @since 1.5.2
|
|
1554
|
+
*/
|
|
1555
|
+
readonly state: PanelState | null;
|
|
1556
|
+
/**
|
|
1557
|
+
* Run code when this panel's state changes.
|
|
1558
|
+
*
|
|
1559
|
+
* @remarks
|
|
1560
|
+
* Needs `events`: without it the listener runs only after this plugin's
|
|
1561
|
+
* own changes. Called with `null` when the panel is unmounted. Errors go
|
|
1562
|
+
* to {@link Scope.onError}.
|
|
1563
|
+
*
|
|
1564
|
+
* @capability ui.mount and events
|
|
1565
|
+
*
|
|
1566
|
+
* @param listener - Receives the new state.
|
|
1567
|
+
* @returns A handle that removes the listener.
|
|
1568
|
+
*
|
|
1569
|
+
* @example Say when the user switched to another tab
|
|
1570
|
+
* ```ts
|
|
1571
|
+
* const panel = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
|
|
1572
|
+
* panel.onChange((state) => {
|
|
1573
|
+
* if (state?.reason === "otherTab") console.error("list in a background tab");
|
|
1574
|
+
* });
|
|
1575
|
+
* ```
|
|
1576
|
+
*
|
|
1577
|
+
* @since 1.5.2
|
|
1578
|
+
*/
|
|
1579
|
+
onChange(listener: (state: PanelState | null) => void): Disposable;
|
|
1580
|
+
/**
|
|
1581
|
+
* Show this panel: make its tab the current one and show its position.
|
|
1582
|
+
*
|
|
1583
|
+
* @remarks
|
|
1584
|
+
* Sets the position's preference like {@link PanelSlot.setVisible}`(true)`
|
|
1585
|
+
* (remembered; in fullscreen the sidebar overlay pops out). On players
|
|
1586
|
+
* without sidebar tabs (0.4.0 and older) it is just that. Waits for
|
|
1587
|
+
* {@link Panel.ready} first.
|
|
1588
|
+
*
|
|
1589
|
+
* @capability ui.mount
|
|
1590
|
+
*
|
|
1591
|
+
* @returns Resolves once the layout shows the panel's tab, with `state`
|
|
1592
|
+
* refreshed.
|
|
1593
|
+
*
|
|
1594
|
+
* @throws `invalid` (rejects) The panel is the backdrop, which the player
|
|
1595
|
+
* shows by itself.
|
|
1596
|
+
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1597
|
+
* @throws The errors of {@link Panel.ready} (rejects) The mount failed.
|
|
1598
|
+
*
|
|
1599
|
+
* @example
|
|
1600
|
+
* ```ts
|
|
1601
|
+
* const list = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
|
|
1602
|
+
* plugin.commands.add({ id: "show", title: "Show the list", run: () => list.show() });
|
|
1603
|
+
* ```
|
|
1604
|
+
*
|
|
1605
|
+
* @since 1.5.2
|
|
1606
|
+
*/
|
|
1607
|
+
show(): Promise<void>;
|
|
1608
|
+
/**
|
|
1609
|
+
* Show this panel, or hide its position when it is shown.
|
|
1610
|
+
*
|
|
1611
|
+
* @remarks
|
|
1612
|
+
* Reads the state from the player first. When the position is shown
|
|
1613
|
+
* (`preferred`) and this panel's tab is the current one, it hides the
|
|
1614
|
+
* position; otherwise it does {@link Panel.show}: a panel in a background
|
|
1615
|
+
* tab comes to the front instead of the sidebar closing. A "Show list"
|
|
1616
|
+
* command checked by `state.preferred && state.active` matches it. Waits
|
|
1617
|
+
* for {@link Panel.ready} first.
|
|
1618
|
+
*
|
|
1619
|
+
* @capability ui.mount
|
|
1620
|
+
*
|
|
1621
|
+
* @returns Resolves once the layout has changed, with `state` refreshed.
|
|
1622
|
+
*
|
|
1623
|
+
* @throws `invalid` (rejects) The panel is the backdrop.
|
|
1624
|
+
* @throws `unavailable` (rejects) The player runs without a user interface.
|
|
1625
|
+
* @throws The errors of {@link Panel.ready} (rejects) The mount failed.
|
|
1626
|
+
*
|
|
1627
|
+
* @example A toggle command for a sidebar panel (the playlist's F6)
|
|
1628
|
+
* ```ts
|
|
1629
|
+
* const list = plugin.panels.mount("sidebar", "ui/list.slint", { title: "List" });
|
|
1630
|
+
* plugin.commands.add({
|
|
1631
|
+
* id: "list",
|
|
1632
|
+
* path: ["@screen", "List"],
|
|
1633
|
+
* bindings: ["F6"],
|
|
1634
|
+
* checked: () => (list.state?.preferred ?? false) && (list.state?.active ?? false),
|
|
1635
|
+
* run: () => list.toggle(),
|
|
1636
|
+
* });
|
|
1637
|
+
* ```
|
|
1638
|
+
*
|
|
1639
|
+
* @since 1.5.2
|
|
1640
|
+
*/
|
|
1641
|
+
toggle(): Promise<void>;
|
|
1439
1642
|
/**
|
|
1440
1643
|
* Keep the panel on screen while the user works in it.
|
|
1441
1644
|
*
|
|
@@ -1489,8 +1692,8 @@ export interface Panel extends View, Disposable {
|
|
|
1489
1692
|
* @returns Resolves once the menu is open (not when the user picks).
|
|
1490
1693
|
*
|
|
1491
1694
|
* @throws `missing` (rejects) An item is not a command of this plugin.
|
|
1492
|
-
* @throws `unavailable` (rejects) The panel is not on screen
|
|
1493
|
-
* backdrop.
|
|
1695
|
+
* @throws `unavailable` (rejects) The panel is not on screen (also: its
|
|
1696
|
+
* sidebar tab is not the current one), or it is the backdrop.
|
|
1494
1697
|
* @throws `invalid` (rejects) No items, more than 32, or `at` is not
|
|
1495
1698
|
* finite.
|
|
1496
1699
|
* @throws The errors of {@link Panel.ready} (rejects) The mount failed.
|
|
@@ -2032,9 +2235,17 @@ export interface Panels {
|
|
|
2032
2235
|
* floats over the video and hides after 3 seconds without activity.
|
|
2033
2236
|
* - `"sidebar"`: right of the video, the window's full height, as wide as the
|
|
2034
2237
|
* user drags it (at least 317). The user can detach it into a floating
|
|
2035
|
-
* window titled `title`.
|
|
2238
|
+
* window titled `title`. Several plugins may mount here (player 0.4.x
|
|
2239
|
+
* after 0.4.0): each panel is a tab, labelled `title` (else the plugin's
|
|
2240
|
+
* name), and the player draws a 31px tab strip above the current one
|
|
2241
|
+
* while there are two or more (with one panel, nothing changes).
|
|
2242
|
+
* Showing, hiding and detaching apply to the whole sidebar. If the
|
|
2243
|
+
* bridge global declares `in property <bool> tabbed`, the player sets
|
|
2244
|
+
* it to `true` while it draws the strip, so a panel with a tab strip of
|
|
2245
|
+
* its own can hide it. Panel coordinates (drops) start below the strip.
|
|
2036
2246
|
* - `"backdrop"`: the whole video area, under the OSD, while there is no
|
|
2037
|
-
* video picture (see {@link Panels.backdrop}). One plugin at a time
|
|
2247
|
+
* video picture (see {@link Panels.backdrop}). One plugin at a time (as
|
|
2248
|
+
* for the bottom bar, another one's mount rejects with `conflict`); it
|
|
2038
2249
|
* takes no pointer or keyboard input. While it is mounted, the player does
|
|
2039
2250
|
* not show the cover art of audio files itself (from the next file
|
|
2040
2251
|
* opened on), so the plugin can draw it.
|
|
@@ -2560,7 +2771,12 @@ export interface PlayerState {
|
|
|
2560
2771
|
readonly kind: "media" | "stream";
|
|
2561
2772
|
/** The source's life cycle. */
|
|
2562
2773
|
readonly phase: "empty" | "loading" | "ready" | "ended" | "error";
|
|
2563
|
-
/**
|
|
2774
|
+
/**
|
|
2775
|
+
* Path or URL; `null` when empty. A generated stream started with
|
|
2776
|
+
* {@link Player.openStream} has the opaque `producer://<n>` (do not parse
|
|
2777
|
+
* it); one a file resolver generates ({@link FileResolverOptions}) keeps
|
|
2778
|
+
* the path that was opened. Tell streams apart by `kind`, not by `source`.
|
|
2779
|
+
*/
|
|
2564
2780
|
readonly source: string | null;
|
|
2565
2781
|
/** Playback is paused. */
|
|
2566
2782
|
readonly paused: boolean;
|
|
@@ -2876,7 +3092,9 @@ export interface Player {
|
|
|
2876
3092
|
*
|
|
2877
3093
|
* It does not wait for playback: use {@link Media.ready}. `paused` opens
|
|
2878
3094
|
* it paused; aborting `signal` closes it. URLs matching a registered
|
|
2879
|
-
* {@link Resolver} are resolved first
|
|
3095
|
+
* {@link Resolver} are resolved first, and so are local files of an
|
|
3096
|
+
* extension a file resolver claims (the source is then a stream that
|
|
3097
|
+
* plugin generates; `paused` reaches it).
|
|
2880
3098
|
*/
|
|
2881
3099
|
open(source: string, options?: {
|
|
2882
3100
|
paused?: boolean;
|
|
@@ -3600,8 +3818,13 @@ export interface MediaCapture {
|
|
|
3600
3818
|
*/
|
|
3601
3819
|
capture(options?: CaptureOptions): Promise<Snapshot>;
|
|
3602
3820
|
}
|
|
3603
|
-
/**
|
|
3604
|
-
|
|
3821
|
+
/**
|
|
3822
|
+
* Declaration of {@link Resolvers.add}: a web resolver
|
|
3823
|
+
* ({@link WebResolverOptions}) or a file resolver ({@link FileResolverOptions}).
|
|
3824
|
+
*/
|
|
3825
|
+
export type ResolverOptions = WebResolverOptions | FileResolverOptions;
|
|
3826
|
+
/** A resolver for web pages: `https://host/…` to playable stream URLs. */
|
|
3827
|
+
export interface WebResolverOptions {
|
|
3605
3828
|
/** Local id, unique within the plugin. */
|
|
3606
3829
|
id: string;
|
|
3607
3830
|
/** URL schemes handled. */
|
|
@@ -3611,14 +3834,104 @@ export interface ResolverOptions {
|
|
|
3611
3834
|
/** Turn a page URL into playable stream URLs. */
|
|
3612
3835
|
resolve(url: string, signal: AbortSignal): Promise<ResolvedMedia>;
|
|
3613
3836
|
}
|
|
3837
|
+
/**
|
|
3838
|
+
* A resolver for local files by extension (SDK 1.5.3, player 0.4.1): when
|
|
3839
|
+
* anyone opens such a file with {@link Player.open} (the Open dialog, the
|
|
3840
|
+
* playlist, next / previous, the recent list, a drop that calls `open`),
|
|
3841
|
+
* the player asks this plugin for a stream it generates instead of
|
|
3842
|
+
* playing the file. Needs `source.resolve` and `producer.av`.
|
|
3843
|
+
*
|
|
3844
|
+
* @remarks
|
|
3845
|
+
* The open keeps everything of the original call: its `ifCurrent`, its
|
|
3846
|
+
* operation (the opener's `open` and `media.ready()` settle as for any
|
|
3847
|
+
* file), `paused` (the stream starts paused), and its path as
|
|
3848
|
+
* `state.source`, with `state.kind` `"stream"`. A thrown error fails that
|
|
3849
|
+
* open: `phase` `"error"` with the error's message as `state.error`,
|
|
3850
|
+
* word for word. The stream belongs to this plugin, as with
|
|
3851
|
+
* {@link Player.openStream}.
|
|
3852
|
+
*
|
|
3853
|
+
* Cancellation: `signal` aborts when the open is replaced, closed
|
|
3854
|
+
* (`player.close()`, F4) or times out (30 seconds), or this resolver is
|
|
3855
|
+
* disposed. Cancelled after `resolve` returned but before the stream ran,
|
|
3856
|
+
* the SDK releases the stream and never calls `started`.
|
|
3857
|
+
*
|
|
3858
|
+
* One extension has one resolver: another plugin's resolver for it makes
|
|
3859
|
+
* {@link Resolver.ready} reject with `conflict` without failing activation
|
|
3860
|
+
* (the player shows a warning). When this plugin stops, its extensions are
|
|
3861
|
+
* free again and such files go to the player as before.
|
|
3862
|
+
*
|
|
3863
|
+
* @example
|
|
3864
|
+
* ```ts
|
|
3865
|
+
* const { readFile } = await import("node:fs/promises");
|
|
3866
|
+
* // The plugin's own emulator: one frame per call.
|
|
3867
|
+
* const emulator = (rom: Uint8Array): (() => StreamFrame) => {
|
|
3868
|
+
* const video = new Uint8Array(256 * 240 * 4).fill(rom[0] ?? 0);
|
|
3869
|
+
* return () => ({ video, audio: new Float32Array(1600) });
|
|
3870
|
+
* };
|
|
3871
|
+
* plugin.resolvers.add({
|
|
3872
|
+
* id: "nes",
|
|
3873
|
+
* extensions: ["nes"],
|
|
3874
|
+
* async resolve(path, signal) {
|
|
3875
|
+
* const frame = emulator(await readFile(path, { signal }));
|
|
3876
|
+
* return {
|
|
3877
|
+
* kind: "stream",
|
|
3878
|
+
* width: 256,
|
|
3879
|
+
* height: 240,
|
|
3880
|
+
* frame,
|
|
3881
|
+
* started: (stream) => stream.onStop((error) => error && plugin.osd.show(error.message)),
|
|
3882
|
+
* };
|
|
3883
|
+
* },
|
|
3884
|
+
* });
|
|
3885
|
+
* ```
|
|
3886
|
+
*/
|
|
3887
|
+
export interface FileResolverOptions {
|
|
3888
|
+
/** Local id, unique within the plugin. */
|
|
3889
|
+
id: string;
|
|
3890
|
+
/**
|
|
3891
|
+
* Lower case, without the dot: 1–16 entries of 1–16 lower-case letters or
|
|
3892
|
+
* digits (not `"folder"`). Only absolute local paths match, by their
|
|
3893
|
+
* extension in any case (`/roms/Game.NES`); URLs never do.
|
|
3894
|
+
*/
|
|
3895
|
+
extensions: string[];
|
|
3896
|
+
/** Read the file and describe the stream to generate for it. */
|
|
3897
|
+
resolve(path: string, signal: AbortSignal): Promise<ResolvedStream>;
|
|
3898
|
+
}
|
|
3899
|
+
/**
|
|
3900
|
+
* What a file resolver returns: a stream, with the same frame contract as
|
|
3901
|
+
* {@link StreamOptions} (size limits, `frame()` on the audio clock).
|
|
3902
|
+
*/
|
|
3903
|
+
export interface ResolvedStream {
|
|
3904
|
+
/** Always `"stream"`. */
|
|
3905
|
+
kind: "stream";
|
|
3906
|
+
/** Frame width in pixels. */
|
|
3907
|
+
width: number;
|
|
3908
|
+
/** Frame height in pixels. */
|
|
3909
|
+
height: number;
|
|
3910
|
+
/** Produce the next frame. Never called concurrently or while paused. */
|
|
3911
|
+
frame(): StreamFrame;
|
|
3912
|
+
/**
|
|
3913
|
+
* Called with the running stream once the player plays it, synchronously
|
|
3914
|
+
* after activation and before any {@link Stream.onStop}. Not called when
|
|
3915
|
+
* the open was cancelled first. `stream.stop()` closes the source (if
|
|
3916
|
+
* still current); the plugin stopping stops it too.
|
|
3917
|
+
*/
|
|
3918
|
+
started?(stream: Stream): void;
|
|
3919
|
+
}
|
|
3614
3920
|
/** A registered resolver. Disposing it unregisters it and aborts resolutions in flight. */
|
|
3615
3921
|
export interface Resolver extends Disposable {
|
|
3616
|
-
/**
|
|
3922
|
+
/**
|
|
3923
|
+
* Resolves when the player registered it; rejects otherwise: `conflict`
|
|
3924
|
+
* when another resolver has the extension or the scheme + host (another
|
|
3925
|
+
* plugin's: activation goes on; this plugin's own: activation fails),
|
|
3926
|
+
* `invalid` for a malformed declaration (a player before 0.4.1 also for
|
|
3927
|
+
* `extensions`).
|
|
3928
|
+
*/
|
|
3617
3929
|
readonly ready: Promise<void>;
|
|
3618
3930
|
}
|
|
3619
3931
|
/**
|
|
3620
|
-
* `plugin.resolvers`: turn web page URLs into playable streams
|
|
3621
|
-
* `source.resolve
|
|
3932
|
+
* `plugin.resolvers`: turn web page URLs into playable streams, or local
|
|
3933
|
+
* files into streams this plugin generates. Needs `source.resolve` (file
|
|
3934
|
+
* resolvers also `producer.av`).
|
|
3622
3935
|
*
|
|
3623
3936
|
* @example
|
|
3624
3937
|
* ```ts
|
|
@@ -3635,9 +3948,11 @@ export interface Resolver extends Disposable {
|
|
|
3635
3948
|
*/
|
|
3636
3949
|
export interface Resolvers {
|
|
3637
3950
|
/**
|
|
3638
|
-
* Register a resolver. When anyone opens a matching URL
|
|
3639
|
-
* `resolve`; `signal` aborts when the open is cancelled or
|
|
3640
|
-
* scheme + host pair has one resolver
|
|
3951
|
+
* Register a resolver. When anyone opens a matching URL or file the
|
|
3952
|
+
* player calls `resolve`; `signal` aborts when the open is cancelled or
|
|
3953
|
+
* replaced. One scheme + host pair, and one extension, has one resolver
|
|
3954
|
+
* (`conflict` otherwise). A rejected `resolve` fails the open with the
|
|
3955
|
+
* error's message.
|
|
3641
3956
|
*/
|
|
3642
3957
|
add(options: ResolverOptions): Resolver;
|
|
3643
3958
|
}
|
|
@@ -3800,6 +4115,38 @@ export interface PluginInfo {
|
|
|
3800
4115
|
* the player has none. The path's layout is not part of the API.
|
|
3801
4116
|
*/
|
|
3802
4117
|
dataDirectory: string | null;
|
|
4118
|
+
/**
|
|
4119
|
+
* What a running plugin was refused because another plugin holds it: a
|
|
4120
|
+
* panel position (`slot-occupied`, with `slot`) or the next/previous
|
|
4121
|
+
* sequence (`sequence-taken`). Neither fails activation; `error` has the
|
|
4122
|
+
* same as text. Empty from players before 0.4.x (after 0.4.0).
|
|
4123
|
+
* Always set by the SDK; optional so that code building `PluginInfo`
|
|
4124
|
+
* values itself (tests) keeps compiling.
|
|
4125
|
+
*
|
|
4126
|
+
* @since 1.5.2
|
|
4127
|
+
*/
|
|
4128
|
+
warnings?: readonly PluginWarning[];
|
|
4129
|
+
}
|
|
4130
|
+
/**
|
|
4131
|
+
* One refusal in {@link PluginInfo.warnings}.
|
|
4132
|
+
*
|
|
4133
|
+
* @since 1.5.2
|
|
4134
|
+
*/
|
|
4135
|
+
export interface PluginWarning {
|
|
4136
|
+
/**
|
|
4137
|
+
* `"slot-occupied"`, `"sequence-taken"` or `"resolver-taken"` (player
|
|
4138
|
+
* 0.4.1); a newer player may add codes.
|
|
4139
|
+
*/
|
|
4140
|
+
code: string;
|
|
4141
|
+
/** The panel position (`"bottom"`, `"sidebar"`, `"backdrop"`) for `slot-occupied`. */
|
|
4142
|
+
slot?: string;
|
|
4143
|
+
/**
|
|
4144
|
+
* What was taken, for `resolver-taken`: 「文件类型 .nes」 or the
|
|
4145
|
+
* `scheme://host` of a web resolver.
|
|
4146
|
+
*/
|
|
4147
|
+
target?: string;
|
|
4148
|
+
/** The manifest id of the plugin holding it (absent for a full sidebar). */
|
|
4149
|
+
owner?: string;
|
|
3803
4150
|
}
|
|
3804
4151
|
/**
|
|
3805
4152
|
* `plugin.packages`: the plugins the player knows. `list` and `onChange`
|
package/dist/plugin/ui.js
CHANGED
|
@@ -323,6 +323,7 @@ export function exported(directory, file, kind) {
|
|
|
323
323
|
}
|
|
324
324
|
class PanelHandle extends ViewBase {
|
|
325
325
|
ready;
|
|
326
|
+
#slot;
|
|
326
327
|
#mounted = false;
|
|
327
328
|
#locks = { focused: false, dragging: false, popupOpen: false };
|
|
328
329
|
#interaction;
|
|
@@ -330,6 +331,7 @@ class PanelHandle extends ViewBase {
|
|
|
330
331
|
if (scope.runtime.views.has("$mount"))
|
|
331
332
|
throw new Error("a plugin can mount only one panel");
|
|
332
333
|
super(scope);
|
|
334
|
+
this.#slot = slot;
|
|
333
335
|
const runtime = scope.runtime;
|
|
334
336
|
runtime.views.set("$mount", (callback, args) => this.dispatch(callback, args));
|
|
335
337
|
this.#interaction = new Latest((locks) => runtime.host.ui.interaction(locks));
|
|
@@ -364,6 +366,51 @@ class PanelHandle extends ViewBase {
|
|
|
364
366
|
write(property, value) {
|
|
365
367
|
return this.scope.runtime.host.ui.setMount(property, value);
|
|
366
368
|
}
|
|
369
|
+
get state() {
|
|
370
|
+
const service = layoutOf(this.scope.runtime);
|
|
371
|
+
service.start();
|
|
372
|
+
return this.live()
|
|
373
|
+
? panelState(service.state, this.#slot, this.scope.runtime.manifest.id)
|
|
374
|
+
: null;
|
|
375
|
+
}
|
|
376
|
+
onChange(listener) {
|
|
377
|
+
const scope = this.scope;
|
|
378
|
+
let last = JSON.stringify(this.state);
|
|
379
|
+
const watch = layoutOf(scope.runtime).watch(() => {
|
|
380
|
+
const state = this.state;
|
|
381
|
+
const next = JSON.stringify(state);
|
|
382
|
+
if (next === last)
|
|
383
|
+
return;
|
|
384
|
+
last = next;
|
|
385
|
+
scope.run(listener, state);
|
|
386
|
+
});
|
|
387
|
+
return scope.add({
|
|
388
|
+
dispose: async () => {
|
|
389
|
+
scope.forget(watch);
|
|
390
|
+
await watch.dispose();
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
async show() {
|
|
395
|
+
await this.ready;
|
|
396
|
+
if (this.#slot === "backdrop")
|
|
397
|
+
throw failure("invalid", "the player shows the backdrop by itself");
|
|
398
|
+
await layoutOf(this.scope.runtime).set(this.#slot, {
|
|
399
|
+
active: this.scope.runtime.manifest.id,
|
|
400
|
+
visible: true,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
async toggle() {
|
|
404
|
+
await this.ready;
|
|
405
|
+
if (this.#slot === "backdrop")
|
|
406
|
+
throw failure("invalid", "the player shows the backdrop by itself");
|
|
407
|
+
await layoutOf(this.scope.runtime).read();
|
|
408
|
+
const state = this.state;
|
|
409
|
+
if (state?.preferred && state.active)
|
|
410
|
+
await layoutOf(this.scope.runtime).set(this.#slot, { visible: false });
|
|
411
|
+
else
|
|
412
|
+
await this.show();
|
|
413
|
+
}
|
|
367
414
|
async showMenu(items, at) {
|
|
368
415
|
await this.ready;
|
|
369
416
|
await this.scope.runtime.host.ui.menu(items.map((item) => item?.id ?? null), at.x, at.y);
|
|
@@ -388,8 +435,38 @@ class PanelHandle extends ViewBase {
|
|
|
388
435
|
this.#mounted = false;
|
|
389
436
|
await this.release();
|
|
390
437
|
this.scope.runtime.views.delete("$mount");
|
|
438
|
+
// Listeners of `onChange` hear `null`.
|
|
439
|
+
this.scope.runtime.invalidate();
|
|
440
|
+
await layoutOf(this.scope.runtime).refresh();
|
|
391
441
|
}
|
|
392
442
|
}
|
|
443
|
+
/** One panel's view of its position (`Panel.state`). */
|
|
444
|
+
function panelState(layout, slot, plugin) {
|
|
445
|
+
if (slot === "backdrop")
|
|
446
|
+
return (layout.backdrop && {
|
|
447
|
+
active: true,
|
|
448
|
+
preferred: true,
|
|
449
|
+
effective: layout.backdrop.effective,
|
|
450
|
+
reason: null,
|
|
451
|
+
detached: false,
|
|
452
|
+
});
|
|
453
|
+
const state = layout[slot];
|
|
454
|
+
if (!state)
|
|
455
|
+
return null;
|
|
456
|
+
// Players without tabs (no `panels`): the position holds this panel only.
|
|
457
|
+
const panels = layout.tabs[slot];
|
|
458
|
+
const own = panels?.find((panel) => panel.plugin === plugin);
|
|
459
|
+
if (panels && !own)
|
|
460
|
+
return null;
|
|
461
|
+
const active = own?.active ?? true;
|
|
462
|
+
return {
|
|
463
|
+
active,
|
|
464
|
+
preferred: state.preferred,
|
|
465
|
+
effective: state.effective && active,
|
|
466
|
+
reason: active ? state.reason : "otherTab",
|
|
467
|
+
detached: state.detached,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
393
470
|
export const windows = (scope) => ({
|
|
394
471
|
create: (file, options) => new WindowHandle(scope, file, options?.id ?? basename(file, extname(file))),
|
|
395
472
|
});
|
|
@@ -400,7 +477,12 @@ export const windows = (scope) => ({
|
|
|
400
477
|
*/
|
|
401
478
|
class LayoutService {
|
|
402
479
|
#runtime;
|
|
403
|
-
state = {
|
|
480
|
+
state = {
|
|
481
|
+
bottom: null,
|
|
482
|
+
sidebar: null,
|
|
483
|
+
tabs: { bottom: null, sidebar: null },
|
|
484
|
+
backdrop: null,
|
|
485
|
+
};
|
|
404
486
|
#listeners = new Set();
|
|
405
487
|
#started = false;
|
|
406
488
|
#reading = false;
|
|
@@ -444,9 +526,17 @@ class LayoutService {
|
|
|
444
526
|
reason: slot.reason,
|
|
445
527
|
detached: slot.detached,
|
|
446
528
|
};
|
|
529
|
+
const tabs = (slot) => slot?.panels
|
|
530
|
+
? Object.freeze(slot.panels.map((panel) => Object.freeze({
|
|
531
|
+
plugin: panel.plugin,
|
|
532
|
+
title: panel.title,
|
|
533
|
+
active: panel.owner === slot.owner,
|
|
534
|
+
})))
|
|
535
|
+
: null;
|
|
447
536
|
const next = {
|
|
448
537
|
bottom: view(raw.bottom),
|
|
449
538
|
sidebar: view(raw.sidebar),
|
|
539
|
+
tabs: { bottom: tabs(raw.bottom), sidebar: tabs(raw.sidebar) },
|
|
450
540
|
backdrop: raw.backdrop
|
|
451
541
|
? Object.freeze({ effective: raw.backdrop.effective === true })
|
|
452
542
|
: null,
|
|
@@ -477,10 +567,15 @@ function slot(scope, name) {
|
|
|
477
567
|
service().start();
|
|
478
568
|
return service().state[name];
|
|
479
569
|
},
|
|
570
|
+
get panels() {
|
|
571
|
+
service().start();
|
|
572
|
+
return service().state.tabs[name];
|
|
573
|
+
},
|
|
480
574
|
onChange(listener) {
|
|
481
|
-
|
|
575
|
+
const seen = (layout) => JSON.stringify([layout[name], layout.tabs[name]]);
|
|
576
|
+
let last = seen(service().state);
|
|
482
577
|
const watch = service().watch((layout) => {
|
|
483
|
-
const next =
|
|
578
|
+
const next = seen(layout);
|
|
484
579
|
if (next === last)
|
|
485
580
|
return;
|
|
486
581
|
last = next;
|
|
@@ -500,6 +595,7 @@ function slot(scope, name) {
|
|
|
500
595
|
await service().set(name, { visible: !(current?.preferred ?? true) });
|
|
501
596
|
},
|
|
502
597
|
setDetached: (detached) => service().set(name, { detached }),
|
|
598
|
+
activate: (plugin) => service().set(name, { active: plugin }),
|
|
503
599
|
};
|
|
504
600
|
}
|
|
505
601
|
function backdrop(scope) {
|