@runmu.sh/ext-media 1.0.0
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 +6 -0
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/index.js +233 -0
- package/package.json +73 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
- The Media panel as an extension. It was part of the μClient core (`MediaPanel.vue`); it now draws each session's media through `mu.media.watch` and drives it through `mu.media.stop`, `clearImages` and `setOutput` (SDK 1.7). The host still plays every cue and keeps the images and the master volume. Panel id `media`, its copy and its `data-testid`s are unchanged, so saved layouts and selectors keep working.
|
|
6
|
+
- The panel is added to a session's workspace the first time it plays something or shows an image (once per world on this device).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 runmu.sh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Media (`@runmu.sh/ext-media`, id `media`)
|
|
2
|
+
|
|
3
|
+
The Media panel of [μClient](https://runmu.sh) (R-MEDIA): what is playing, the master volume, and the images the game showed.
|
|
4
|
+
|
|
5
|
+
First-party, on the [marketplace](https://runmu.sh/marketplace/x/media). Install it from **☰ → Extensions → Discover** and enable it per world in **Extensions → Installed**.
|
|
6
|
+
|
|
7
|
+
Until 2026-10 the panel was part of the client core. The core still plays the audio and keeps each session's images and the master volume; this extension only draws them. Without it, sound still plays and the ☰ menu's Sound row still sets the volume.
|
|
8
|
+
|
|
9
|
+
## Where the media comes from
|
|
10
|
+
The host keeps one media state per session and fills it from:
|
|
11
|
+
|
|
12
|
+
| Source | What |
|
|
13
|
+
|---|---|
|
|
14
|
+
| GMCP `Client.Media.Default`, `Client.Media.Play`, `Client.Media.Stop` | Music and sounds (`key`, `name`, `url`, `type`, `volume`, `loops`) |
|
|
15
|
+
| MSP `!!SOUND(...)` / `!!MUSIC(...)` | Music and sounds |
|
|
16
|
+
| MXP `<image>` | Images (URL and caption), newest last |
|
|
17
|
+
| Another extension: `mu.media.play(spec)` / `mu.media.stop(filter)` | Music and sounds |
|
|
18
|
+
|
|
19
|
+
The core owns `Client.Media` support (it negotiates the package and plays the cues), so the manifest declares no GMCP package. The extension sends nothing to the game; its one capability is `read-output`, because it listens to `mu.sessions.on('line')` to notice a session opened in the background.
|
|
20
|
+
|
|
21
|
+
## What it looks like
|
|
22
|
+
Composed as Underspire's media panel, on `--bg-elev`:
|
|
23
|
+
|
|
24
|
+
- **NOW PLAYING** (an uppercase `--accent-bright` tag; the block has an `--accent` bottom rule): one row per track, music first then sounds: `♪`, the title (the file name without folder or extension, in `--gold`), the kind (`music` / `sound`, small faint capitals) and **[ STOP ]**, which stops that track. With nothing playing it says NOTHING PLAYING.
|
|
25
|
+
- When the browser blocked autoplay: *click anywhere to allow audio* in `--gold`.
|
|
26
|
+
- The volume row: the **VOL** label (struck through and faint when muted or at 0), the host's `.rng` slider (filled to `--p`), and the value, or `—` when muted. The slider is the master volume, the same setting as the ☰ menu's Sound row. Moving it off 0 unmutes.
|
|
27
|
+
- **IMAGES** with **[ CLEAR ]** (forgets the session's images) over a grid of 72px thumbnails; each opens the image in a new tab, its caption as the tooltip. With none it says NO IMAGES.
|
|
28
|
+
|
|
29
|
+
## Behaviour
|
|
30
|
+
- Panel `media`, singleton, per session, default position right top, Views order 30 (after Scene and Channels).
|
|
31
|
+
- The panel auto-adds on the first track or image seen: the first time a session plays music or a sound, or shows an image, the panel is added if it is not open (R-AUTO-PANELS), once per world on this device, so a Media panel you closed stays closed. It watches every open session (`mu.sessions.list()`, re-read on `switch` and on a line from a session it has not seen), and stops watching a session once its panel was added or when the session closes.
|
|
32
|
+
- No settings.
|
|
33
|
+
|
|
34
|
+
## SDK
|
|
35
|
+
Uses `mu.panels.register` (with `order`), `mu.panels.vue` (render functions, `h` from the host's Vue), `mu.panels.autoAdd`, `mu.media.watch`, `mu.media.stop`, `mu.media.clearImages`, `mu.media.setOutput` (SDK 1.7), `mu.sessions.list` / `on` and `mu.ui.style`. It exports no API. The pure helpers (`tracksOf`, `titleOf`, `pctOf`, `volumePatch`, `isStruck`, `volumeText`, `hasMedia`, `watchSessions`) are in `src/model.ts` for the tests.
|
|
36
|
+
|
|
37
|
+
## Develop
|
|
38
|
+
Made with `npm create @runmu.sh/extension` ([the quickstart](https://runmu.sh/docs/extensions/quickstart)).
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install
|
|
42
|
+
npm run build # src/index.ts → dist/index.js, then the manifest check
|
|
43
|
+
npm run typecheck # tsc --noEmit against @runmu.sh/sdk
|
|
44
|
+
npm test # node --experimental-strip-types --test "tests/*.test.mjs"
|
|
45
|
+
npm run dev # dev server on http://localhost:5199/ with hot reload
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
In μClient: **☰ → Extensions → Advanced → Developer → load from dev server**.
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { defineExtension } from "@muclient/sdk";
|
|
3
|
+
|
|
4
|
+
// src/panel.ts
|
|
5
|
+
import { defineComponent, h, onBeforeUnmount, shallowRef, watch } from "vue";
|
|
6
|
+
|
|
7
|
+
// src/model.ts
|
|
8
|
+
var COPY = {
|
|
9
|
+
title: "Media",
|
|
10
|
+
nowPlaying: "Now playing",
|
|
11
|
+
nothing: "Nothing playing",
|
|
12
|
+
images: "Images",
|
|
13
|
+
noImages: "No images",
|
|
14
|
+
clear: "Clear",
|
|
15
|
+
stopLabel: "Stop",
|
|
16
|
+
stop: (name) => `stop ${name}`,
|
|
17
|
+
volume: "volume",
|
|
18
|
+
vol: "Vol",
|
|
19
|
+
blocked: "click anywhere to allow audio",
|
|
20
|
+
open: "open media"
|
|
21
|
+
};
|
|
22
|
+
var MEDIA_CSS = `
|
|
23
|
+
.mu-media { height: 100%; display: flex; flex-direction: column; overflow-y: auto; background: var(--bg-elev); box-sizing: border-box; }
|
|
24
|
+
.mu-media .np { flex: none; border-bottom: 1px solid var(--accent); }
|
|
25
|
+
.mu-media .hd { display: flex; align-items: center; padding: 6px 10px; border-bottom: 1px solid var(--border); }
|
|
26
|
+
.mu-media .tag { font-size: .72rem; letter-spacing: .2em; text-transform: uppercase; color: var(--accent-bright); }
|
|
27
|
+
.mu-media .x { margin-left: auto; }
|
|
28
|
+
.mu-media .tracks { list-style: none; margin: 0; padding: 8px 10px 0; display: flex; flex-direction: column; gap: 4px; font-size: .78rem; }
|
|
29
|
+
.mu-media .tracks li { display: flex; align-items: center; gap: 8px; }
|
|
30
|
+
.mu-media .g { color: var(--accent); }
|
|
31
|
+
.mu-media .t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--gold); }
|
|
32
|
+
.mu-media .k { font-size: .6rem; letter-spacing: .2em; text-transform: uppercase; color: var(--fg-faint); }
|
|
33
|
+
.mu-media .empty { margin: 0; padding: 10px; color: var(--fg-faint); font-style: normal; font-size: .64rem; letter-spacing: .14em; text-transform: uppercase; }
|
|
34
|
+
.mu-media .blocked { padding-top: 0; color: var(--gold); }
|
|
35
|
+
.mu-media .vol { display: flex; align-items: center; gap: 8px; padding: 6px 10px 9px; }
|
|
36
|
+
.mu-media .vol .rng { flex: 1; max-width: none; }
|
|
37
|
+
.mu-media .vglyph { color: var(--fg-dim); font-size: .62rem; letter-spacing: .16em; text-transform: uppercase; transition: color .12s ease; }
|
|
38
|
+
.mu-media .vglyph.muted { color: var(--fg-faint); text-decoration: line-through; }
|
|
39
|
+
.mu-media .vval { min-width: 2em; text-align: right; font-size: .72rem; color: var(--fg-dim); }
|
|
40
|
+
.mu-media .gallery { flex: 1; }
|
|
41
|
+
.mu-media .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); gap: 5px; padding: 8px 10px; }
|
|
42
|
+
.mu-media .thumb { display: block; aspect-ratio: 1; overflow: hidden; border: 1px solid var(--border-bright); background: var(--bg); transition: border-color .12s ease; }
|
|
43
|
+
.mu-media .thumb:hover { border-color: var(--accent); }
|
|
44
|
+
.mu-media .thumb:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: -2px; }
|
|
45
|
+
.mu-media .thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
46
|
+
`;
|
|
47
|
+
function tracksOf(m) {
|
|
48
|
+
return m ? [...m.music ? [m.music] : [], ...m.sounds] : [];
|
|
49
|
+
}
|
|
50
|
+
function titleOf(t) {
|
|
51
|
+
if (t.title) return t.title;
|
|
52
|
+
return (t.name.split("/").pop() || t.name).replace(/\.[a-z0-9]{2,4}$/i, "");
|
|
53
|
+
}
|
|
54
|
+
function pctOf(volume) {
|
|
55
|
+
const v = Math.round(Number(volume) * 100);
|
|
56
|
+
return Number.isFinite(v) ? Math.min(100, Math.max(0, v)) : 0;
|
|
57
|
+
}
|
|
58
|
+
function volumePatch(pct, muted) {
|
|
59
|
+
const p = Math.min(100, Math.max(0, Math.round(Number(pct) || 0)));
|
|
60
|
+
return p > 0 && muted ? { volume: p / 100, muted: false } : { volume: p / 100 };
|
|
61
|
+
}
|
|
62
|
+
var isStruck = (muted, pct) => muted || pct === 0;
|
|
63
|
+
var volumeText = (muted, pct) => muted ? "\u2014" : `${pct}%`;
|
|
64
|
+
var imageTitle = (i) => i.caption || i.url;
|
|
65
|
+
function hasMedia(m) {
|
|
66
|
+
return !!m && (!!m.music || m.sounds.length > 0 || m.images.length > 0);
|
|
67
|
+
}
|
|
68
|
+
function watchSessions(mu, panel = "media") {
|
|
69
|
+
const watches = /* @__PURE__ */ new Map();
|
|
70
|
+
const added = /* @__PURE__ */ new Set();
|
|
71
|
+
let live = true;
|
|
72
|
+
const isLive = (sid) => mu.sessions.list().some((s) => s.id === sid);
|
|
73
|
+
const stop = (sid) => {
|
|
74
|
+
const off = watches.get(sid);
|
|
75
|
+
watches.delete(sid);
|
|
76
|
+
off?.();
|
|
77
|
+
};
|
|
78
|
+
const ensure = (sid) => {
|
|
79
|
+
if (!live || !sid || added.has(sid) || watches.has(sid)) return;
|
|
80
|
+
let ended = false;
|
|
81
|
+
const end = () => {
|
|
82
|
+
ended = true;
|
|
83
|
+
stop(sid);
|
|
84
|
+
};
|
|
85
|
+
const off = mu.media.watch((m) => {
|
|
86
|
+
if (ended) return;
|
|
87
|
+
if (!isLive(sid)) {
|
|
88
|
+
end();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!hasMedia(m)) return;
|
|
92
|
+
added.add(sid);
|
|
93
|
+
mu.panels.autoAdd(panel, sid);
|
|
94
|
+
end();
|
|
95
|
+
}, sid);
|
|
96
|
+
if (ended) off();
|
|
97
|
+
else watches.set(sid, off);
|
|
98
|
+
};
|
|
99
|
+
const sync = () => {
|
|
100
|
+
if (!live) return;
|
|
101
|
+
const ids = new Set(mu.sessions.list().map((s) => s.id));
|
|
102
|
+
for (const sid of [...watches.keys()]) if (!ids.has(sid)) stop(sid);
|
|
103
|
+
for (const sid of [...added]) if (!ids.has(sid)) added.delete(sid);
|
|
104
|
+
for (const sid of ids) ensure(sid);
|
|
105
|
+
};
|
|
106
|
+
const offSwitch = mu.sessions.on("switch", sync);
|
|
107
|
+
const offLine = mu.sessions.on("line", (_l, { sid }) => {
|
|
108
|
+
if (!watches.has(sid) && !added.has(sid)) sync();
|
|
109
|
+
});
|
|
110
|
+
sync();
|
|
111
|
+
return () => {
|
|
112
|
+
live = false;
|
|
113
|
+
offSwitch();
|
|
114
|
+
offLine();
|
|
115
|
+
for (const sid of [...watches.keys()]) stop(sid);
|
|
116
|
+
added.clear();
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/panel.ts
|
|
121
|
+
function createPanel(mu) {
|
|
122
|
+
const c = mu.ui.css;
|
|
123
|
+
const empty = (...more) => [.../* @__PURE__ */ new Set([c.empty, "empty", ...more])].join(" ");
|
|
124
|
+
return defineComponent({
|
|
125
|
+
name: "MediaPanel",
|
|
126
|
+
props: { sid: { type: String, default: null }, worldId: { type: String, default: null }, params: { type: Object, default: () => ({}) } },
|
|
127
|
+
setup(props) {
|
|
128
|
+
const m = shallowRef(null);
|
|
129
|
+
let off = null;
|
|
130
|
+
const attach = (sid) => {
|
|
131
|
+
off?.();
|
|
132
|
+
off = null;
|
|
133
|
+
m.value = null;
|
|
134
|
+
if (sid) off = mu.media.watch((v) => {
|
|
135
|
+
m.value = v;
|
|
136
|
+
}, sid);
|
|
137
|
+
};
|
|
138
|
+
watch(() => props.sid, attach, { immediate: true });
|
|
139
|
+
onBeforeUnmount(() => {
|
|
140
|
+
off?.();
|
|
141
|
+
off = null;
|
|
142
|
+
});
|
|
143
|
+
const onVolume = (e) => {
|
|
144
|
+
const v = Number(e.target.value);
|
|
145
|
+
mu.media.setOutput(volumePatch(v, !!m.value?.muted));
|
|
146
|
+
};
|
|
147
|
+
return () => {
|
|
148
|
+
const sid = props.sid;
|
|
149
|
+
const v = m.value;
|
|
150
|
+
const tracks = tracksOf(v);
|
|
151
|
+
const muted = !!v?.muted;
|
|
152
|
+
const pct = pctOf(v?.volume ?? 0);
|
|
153
|
+
const images = v?.images ?? [];
|
|
154
|
+
const head = (label, tool) => h("div", { class: "hd" }, [h("span", { class: `tag ${c.glow}` }, label), tool ?? null]);
|
|
155
|
+
return h("section", { class: "mu-media", "aria-label": COPY.title, "data-testid": "media" }, [
|
|
156
|
+
h("div", { class: "np" }, [
|
|
157
|
+
head(COPY.nowPlaying),
|
|
158
|
+
tracks.length ? h("ul", { class: "tracks", "data-testid": "media-tracks" }, tracks.map((t) => h("li", { key: t.kind + t.key, class: t.kind }, [
|
|
159
|
+
h("span", { class: "g", "aria-hidden": "true" }, "\u266A"),
|
|
160
|
+
h("span", { class: "t" }, titleOf(t)),
|
|
161
|
+
h("span", { class: "k" }, t.kind),
|
|
162
|
+
h("button", {
|
|
163
|
+
type: "button",
|
|
164
|
+
class: `${c.cmd} x`,
|
|
165
|
+
"aria-label": COPY.stop(titleOf(t)),
|
|
166
|
+
onClick: () => {
|
|
167
|
+
if (sid) mu.media.stop({ key: t.key, type: t.kind }, sid);
|
|
168
|
+
}
|
|
169
|
+
}, COPY.stopLabel)
|
|
170
|
+
]))) : h("p", { class: empty(), "data-testid": "media-empty" }, COPY.nothing),
|
|
171
|
+
v?.blocked ? h("p", { class: empty("blocked") }, COPY.blocked) : null,
|
|
172
|
+
h("label", { class: "vol" }, [
|
|
173
|
+
h("span", { class: ["vglyph", { muted: isStruck(muted, pct) }], "aria-hidden": "true", "data-testid": "media-vglyph" }, COPY.vol),
|
|
174
|
+
h("input", {
|
|
175
|
+
type: "range",
|
|
176
|
+
min: 0,
|
|
177
|
+
max: 100,
|
|
178
|
+
step: 1,
|
|
179
|
+
class: "rng",
|
|
180
|
+
value: pct,
|
|
181
|
+
style: { "--p": `${pct}%` },
|
|
182
|
+
"aria-label": COPY.volume,
|
|
183
|
+
"data-testid": "media-volume",
|
|
184
|
+
onInput: onVolume
|
|
185
|
+
}),
|
|
186
|
+
h("span", { class: "vval" }, volumeText(muted, pct))
|
|
187
|
+
])
|
|
188
|
+
]),
|
|
189
|
+
h("div", { class: "gallery" }, [
|
|
190
|
+
head(COPY.images, images.length ? h("button", { type: "button", class: `${c.cmd} x`, onClick: () => {
|
|
191
|
+
if (sid) mu.media.clearImages(sid);
|
|
192
|
+
} }, COPY.clear) : null),
|
|
193
|
+
images.length ? h("div", { class: "grid", "data-testid": "media-gallery" }, images.map((i) => h("a", { key: i.url, class: "thumb", href: i.url, target: "_blank", rel: "noopener", title: imageTitle(i) }, [
|
|
194
|
+
h("img", { src: i.url, alt: i.caption || "", loading: "lazy" })
|
|
195
|
+
]))) : h("p", { class: empty(), "data-testid": "media-noimages" }, COPY.noImages)
|
|
196
|
+
])
|
|
197
|
+
]);
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/index.ts
|
|
204
|
+
var index_default = defineExtension({
|
|
205
|
+
activate(ctx) {
|
|
206
|
+
const mu = ctx.mu;
|
|
207
|
+
mu.ui.style(MEDIA_CSS);
|
|
208
|
+
mu.panels.register({
|
|
209
|
+
id: "media",
|
|
210
|
+
title: COPY.title,
|
|
211
|
+
singleton: true,
|
|
212
|
+
perSession: true,
|
|
213
|
+
defaultPosition: "right-top",
|
|
214
|
+
order: 30,
|
|
215
|
+
mount: mu.panels.vue(createPanel(mu))
|
|
216
|
+
});
|
|
217
|
+
ctx.subscriptions.push(watchSessions(mu, "media"));
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
export {
|
|
221
|
+
COPY,
|
|
222
|
+
MEDIA_CSS,
|
|
223
|
+
index_default as default,
|
|
224
|
+
hasMedia,
|
|
225
|
+
imageTitle,
|
|
226
|
+
isStruck,
|
|
227
|
+
pctOf,
|
|
228
|
+
titleOf,
|
|
229
|
+
tracksOf,
|
|
230
|
+
volumePatch,
|
|
231
|
+
volumeText,
|
|
232
|
+
watchSessions
|
|
233
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@runmu.sh/ext-media",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The Media panel: what is playing (music and sounds, each with Stop), the master volume and mute, and the images the game showed, from GMCP Client.Media, MSP and MXP <image>.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"media",
|
|
7
|
+
"sound",
|
|
8
|
+
"music",
|
|
9
|
+
"images",
|
|
10
|
+
"gmcp",
|
|
11
|
+
"msp",
|
|
12
|
+
"mxp",
|
|
13
|
+
"panel"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"homepage": "https://runmu.sh/marketplace/x/media",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/runmu-sh/ext-media.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/runmu-sh/ext-media/issues"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": "./dist/index.js",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"CHANGELOG.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "node scripts/build.mjs",
|
|
33
|
+
"dev": "node scripts/dev.mjs",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"check": "node scripts/build.mjs --check",
|
|
36
|
+
"test": "node --experimental-strip-types --test \"tests/*.test.mjs\""
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@muclient/sdk": "npm:@runmu.sh/sdk@^1.7.1",
|
|
40
|
+
"@runmu.sh/dev": "^0.1.0",
|
|
41
|
+
"esbuild": "^0.28.2",
|
|
42
|
+
"typescript": "^5.9.0",
|
|
43
|
+
"vue": "^3.5.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=22.6"
|
|
47
|
+
},
|
|
48
|
+
"muclient": {
|
|
49
|
+
"id": "media",
|
|
50
|
+
"api": "^1.7",
|
|
51
|
+
"source": "src/index.ts",
|
|
52
|
+
"displayName": "Media",
|
|
53
|
+
"description": "The Media panel: what is playing with a Stop per track, the master volume and mute, and the images the game showed.",
|
|
54
|
+
"categories": [
|
|
55
|
+
"interface",
|
|
56
|
+
"sound"
|
|
57
|
+
],
|
|
58
|
+
"contributes": {
|
|
59
|
+
"panels": [
|
|
60
|
+
{
|
|
61
|
+
"id": "media",
|
|
62
|
+
"title": "Media",
|
|
63
|
+
"defaultPosition": "right-top",
|
|
64
|
+
"order": 30
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"capabilities": [
|
|
69
|
+
"read-output"
|
|
70
|
+
],
|
|
71
|
+
"publisher": "bitmuse"
|
|
72
|
+
}
|
|
73
|
+
}
|