@vibedev-si/dsh-media-viewer 0.1.0 → 0.1.2
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/README.md +5 -0
- package/client.js +342 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
画廊在右侧栏的 **+ 菜单**里叫「媒体画廊」;每个播放器工具栏里的「画廊」按钮会直接打开它所在的文件夹。
|
|
18
18
|
|
|
19
|
+
## 本版新增(0.1.2)
|
|
20
|
+
|
|
21
|
+
新增「组件」入口:在预览工具栏里可直接打开 **VibeDev 插件中心**,了解或补齐影视工作台与 VibeDev 账号插件(打不开插件中心时会给出可复制的包名);已安装或应用内置的组件不会重复推荐。
|
|
22
|
+
|
|
19
23
|
## 安装
|
|
20
24
|
|
|
21
25
|
**前置依赖:[`dsh-better-sidebar`](https://www.npmjs.com/package/dsh-better-sidebar)**(≥ 0.24.0)。它接管了右侧栏的文件打开,本插件通过它的 `registerFileViewer` / `registerTab` 接入。没装它,本插件不会生效。
|
|
@@ -72,6 +76,7 @@ client 是无需构建的纯 JS(`client.js`),host 是 `index.js`,没有
|
|
|
72
76
|
- **Audio** — mp3 / wav / m4a / flac…, live spectrum, playback speed, loop.
|
|
73
77
|
- **HTML** — scripts allowed by default; relative CSS / JS / images / video / `fetch()` all load; preview ⇄ source, phone / tablet / desktop widths, script toggle. Runs in an opaque-origin sandboxed iframe, so a previewed page cannot reach the GUI.
|
|
74
78
|
- **Media gallery** — thumbnail grid for a folder with type filters, search, sort, optional recursion, and a lightbox with ← / → navigation.
|
|
79
|
+
- **Related components (0.1.2)** — a 「组件」 entry in the preview toolbar opens the **VibeDev plugin centre** from the sidebar to learn about or add the film workbench and the VibeDev account plugin (and falls back to a copyable package name); components already installed or shipped by the app are never recommended again.
|
|
75
80
|
|
|
76
81
|
Why it exists: the built-in `/sidebar/file` and `/sidebar/html` routes cap files at 20 MB, read them fully into memory, have no Range support, and their fence rejects the `cross-site` sub-resource requests a sandboxed iframe makes. This plugin ships its own `/mp/*` routes instead. Install it by package name from your DSH plugin manager, or `dsh plugin --profile <name> add @vibedev-si/dsh-media-viewer`, then reload the page.
|
|
77
82
|
|
package/client.js
CHANGED
|
@@ -81,13 +81,20 @@ window.__ModuleLoader__.load({
|
|
|
81
81
|
|
|
82
82
|
// ── styling helpers ─────────────────────────────────────────────────────
|
|
83
83
|
|
|
84
|
+
// Colours: ONLY variables the host really defines (--dsw-alias-bg-base, -bg-layer-1/2, -bg-overlay, -border-l1/l2,
|
|
85
|
+
// -brand-primary, -label-primary/secondary, -state-*). An earlier version used three names it had guessed
|
|
86
|
+
// (a "bg-primary", a "surface-primary" and a "link-normal"): the host has none of them, so the fallback (#fff)
|
|
87
|
+
// was always used and the gallery lightbox came out white, with light text on it, in the dark theme.
|
|
88
|
+
// test/tokens.test.mjs now rejects any --dsw-* name that is not on the real list.
|
|
84
89
|
var C = {
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
bg: "var(--dsw-alias-bg-base, #fff)",
|
|
91
|
+
fg: "var(--dsw-alias-label-primary, #1b1d24)",
|
|
92
|
+
muted: "var(--dsw-alias-label-secondary, #6c7080)",
|
|
93
|
+
link: "var(--dsw-alias-brand-primary, #2f6fe0)",
|
|
87
94
|
line: "rgba(127,127,127,.28)",
|
|
88
95
|
soft: "rgba(127,127,127,.10)",
|
|
89
96
|
softer: "rgba(127,127,127,.06)",
|
|
90
|
-
accent: "#
|
|
97
|
+
accent: "#7254f5", // a white label on it is 4.88:1 (the old #7c5cff gave 4.35:1)
|
|
91
98
|
};
|
|
92
99
|
|
|
93
100
|
var btnBase = {
|
|
@@ -151,6 +158,12 @@ window.__ModuleLoader__.load({
|
|
|
151
158
|
h("rect", { key: "d", x: 9, y: 9, width: 5, height: 5, rx: 1, stroke: "currentColor", strokeWidth: 1.5 }),
|
|
152
159
|
]);
|
|
153
160
|
}
|
|
161
|
+
function IconTools(size) {
|
|
162
|
+
return svg(size, [
|
|
163
|
+
h("rect", { key: "a", x: 2, y: 2, width: 12, height: 12, rx: 3, stroke: "currentColor", strokeWidth: 1.5 }),
|
|
164
|
+
pathEl("M8 5.4v5.2M5.4 8h5.2", { key: "b" }),
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
154
167
|
function kindIcon(kind, size) {
|
|
155
168
|
return kind === "video" ? IconVideo(size) : kind === "audio" ? IconAudio(size) : kind === "html" ? IconHtml(size) : IconImage(size);
|
|
156
169
|
}
|
|
@@ -173,7 +186,7 @@ window.__ModuleLoader__.load({
|
|
|
173
186
|
} catch (e) { console.error("[dsh-media-viewer] open gallery failed", e); }
|
|
174
187
|
}
|
|
175
188
|
|
|
176
|
-
function CommonActions(props, abs, url) {
|
|
189
|
+
function CommonActions(props, abs, url, toolsOpen, onTools) {
|
|
177
190
|
var out = [
|
|
178
191
|
h("a", { key: "new", href: url, target: "_blank", rel: "noopener", style: btn(), title: "在新窗口打开" }, "新窗口"),
|
|
179
192
|
h("a", { key: "dl", href: fileUrl(props.scope, abs, "download=1"), download: baseName(abs), style: btn(), title: "下载原文件" }, "下载"),
|
|
@@ -181,6 +194,9 @@ window.__ModuleLoader__.load({
|
|
|
181
194
|
if (!props.hideGallery) {
|
|
182
195
|
out.push(h(Btn, { key: "gal", title: "浏览所在文件夹的全部媒体", onClick: function () { openGallery(props, abs); } }, [IconGrid(12), "画廊"]));
|
|
183
196
|
}
|
|
197
|
+
if (typeof onTools === "function") {
|
|
198
|
+
out.push(h(Btn, { key: "tools", active: toolsOpen === true, title: "相关组件与插件中心", onClick: onTools }, [IconTools(12), "组件"]));
|
|
199
|
+
}
|
|
184
200
|
return out;
|
|
185
201
|
}
|
|
186
202
|
|
|
@@ -193,9 +209,305 @@ window.__ModuleLoader__.load({
|
|
|
193
209
|
}
|
|
194
210
|
|
|
195
211
|
function Shell(p) {
|
|
212
|
+
var kids = p.children ? (Array.isArray(p.children) ? p.children.slice() : [p.children]) : [];
|
|
213
|
+
if (p.tools) kids.push(p.tools);
|
|
196
214
|
return h("div", {
|
|
197
215
|
style: { display: "flex", flexDirection: "column", gap: "8px", padding: "10px", minWidth: 0, minHeight: 0, height: "100%", boxSizing: "border-box" },
|
|
198
|
-
},
|
|
216
|
+
}, kids);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── companion components (the plugin centre is the way to the rest) ─────
|
|
220
|
+
//
|
|
221
|
+
// What the Host actually has comes from its plugin inventory — a bundle the profile
|
|
222
|
+
// installed, or one the application ships itself. Every service below is read
|
|
223
|
+
// optionally and strictly, so a Host without a plugin manager, a panel layout, or the
|
|
224
|
+
// plugin navigation contract keeps this viewer working and simply shows the manual
|
|
225
|
+
// spec. Nothing here installs anything: the centre and the Plugins page own every
|
|
226
|
+
// install, approval and progress step, and nothing is read before a surface is opened.
|
|
227
|
+
|
|
228
|
+
/** The VibeDev components by package name; the centre is the way to all of them. */
|
|
229
|
+
var SUITE = { account: "@vibedev-si/dsh-vibedev", center: "@vibedev-si/dsh-ecosystem", film: "dsh-film" };
|
|
230
|
+
/** The main panel the VibeDev plugin centre registers its page under. */
|
|
231
|
+
var CENTER_PANEL = "vibedev-center";
|
|
232
|
+
/** The package a person can add on the Plugins page when nothing can open the centre. */
|
|
233
|
+
var CENTER_SPEC = SUITE.center;
|
|
234
|
+
/**
|
|
235
|
+
* The components this viewer's own work leans on, in the order the card lists them.
|
|
236
|
+
* A bundle already installed or shipped by the app is never named (see `bundlePresent`),
|
|
237
|
+
* and the viewer never recommends itself.
|
|
238
|
+
*/
|
|
239
|
+
var COMPANIONS = [
|
|
240
|
+
{ package: SUITE.account, line: "VibeDev 账号:用余额生成图片、视频和音频,模型请求走 VibeDev 网关。" },
|
|
241
|
+
{ package: SUITE.film, line: "影视工作台:剧本、分镜画布和导演台,把这个文件夹里的媒体排成片子。" },
|
|
242
|
+
];
|
|
243
|
+
/** The card's own copy, in the one language this bundle's UI uses. */
|
|
244
|
+
var SUITE_COPY = {
|
|
245
|
+
missingTitle: "相关组件",
|
|
246
|
+
missingIntro: "这些组件和右侧栏预览配合使用,装好后出现在它们各自的页面里。",
|
|
247
|
+
toolsTitle: "插件中心",
|
|
248
|
+
toolsIntro: "插件中心里可以浏览其余组件。",
|
|
249
|
+
keep: "这里只做提示:不会自动安装,也不会改动已装的东西;安装与授权都在插件中心里完成。",
|
|
250
|
+
open: "打开插件中心",
|
|
251
|
+
manual: "打不开插件中心时,可在「插件」页按包名安装:",
|
|
252
|
+
copy: "复制包名",
|
|
253
|
+
copied: "已复制",
|
|
254
|
+
tools: "组件",
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/** A value read as a record, without trusting its declared type. */
|
|
258
|
+
function asRecord(v) { return typeof v === "object" && v !== null ? v : undefined; }
|
|
259
|
+
|
|
260
|
+
/** One property of a service, read without letting a hostile getter throw into the viewer. */
|
|
261
|
+
function safeGet(target, name) {
|
|
262
|
+
var record = asRecord(target);
|
|
263
|
+
if (record === undefined) return undefined;
|
|
264
|
+
try { return record[name]; } catch (e) { return undefined; }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** One optional service, read strictly: absent (never throwing) on a Host that does not have it. */
|
|
268
|
+
function serviceOf(ctx, name) {
|
|
269
|
+
if (!ctx || typeof ctx.get !== "function") return undefined;
|
|
270
|
+
try { return ctx.get(name); } catch (e) { return undefined; }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The optional services the card uses, from the strict store first and the property proxy second. */
|
|
274
|
+
function suiteServices(ctx) {
|
|
275
|
+
var remote = asRecord(serviceOf(ctx, "remote") || safeGet(ctx, "remote"));
|
|
276
|
+
return {
|
|
277
|
+
manager: asRecord(serviceOf(ctx, "remote.pluginManager") || safeGet(remote, "pluginManager")),
|
|
278
|
+
slots: asRecord(serviceOf(ctx, "slots") || safeGet(ctx, "slots")),
|
|
279
|
+
layout: asRecord(serviceOf(ctx, "layout") || safeGet(ctx, "layout")),
|
|
280
|
+
navigation: asRecord(serviceOf(ctx, "pluginNavigation") || safeGet(ctx, "pluginNavigation")),
|
|
281
|
+
remote: remote,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Whether the Host already has a bundle: the profile installed it, or the application provides it itself. */
|
|
286
|
+
function bundlePresent(bundle) {
|
|
287
|
+
return bundle.installed === true || (bundle.removable === false && !bundle.source);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The present package names of one inventory answer, or undefined when the answer is not a
|
|
292
|
+
* readable list. An answer this cannot read is never read as "nothing is present": a refusal
|
|
293
|
+
* envelope, another shape, or a missing value means the caller knows nothing, not that every
|
|
294
|
+
* component is missing.
|
|
295
|
+
*/
|
|
296
|
+
function presentFrom(answer) {
|
|
297
|
+
var record = asRecord(answer);
|
|
298
|
+
if (record === undefined || record.ok === false) return undefined;
|
|
299
|
+
if (!Array.isArray(record.value)) return undefined;
|
|
300
|
+
var names = [];
|
|
301
|
+
for (var i = 0; i < record.value.length; i++) {
|
|
302
|
+
var bundle = asRecord(record.value[i]);
|
|
303
|
+
if (!bundle || !bundlePresent(bundle)) continue;
|
|
304
|
+
if (typeof bundle.name === "string" && bundle.name !== "" && names.indexOf(bundle.name) < 0) names.push(bundle.name);
|
|
305
|
+
}
|
|
306
|
+
return names;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Read the Host's plugin inventory once. A manager that cannot answer — absent, throwing,
|
|
311
|
+
* rejecting, or answering an envelope that is not a readable list — is not an inventory of
|
|
312
|
+
* nothing: `ok: false` makes the card offer the centre and claim no component is missing.
|
|
313
|
+
*/
|
|
314
|
+
function readPresence(ctx) {
|
|
315
|
+
var manager = suiteServices(ctx).manager;
|
|
316
|
+
var list = safeGet(manager, "listBundles");
|
|
317
|
+
if (typeof list !== "function") return Promise.resolve({ ok: false, present: [] });
|
|
318
|
+
try {
|
|
319
|
+
return Promise.resolve(list.call(manager)).then(
|
|
320
|
+
function (answer) {
|
|
321
|
+
var present = presentFrom(answer);
|
|
322
|
+
return present === undefined ? { ok: false, present: [] } : { ok: true, present: present };
|
|
323
|
+
},
|
|
324
|
+
function () { return { ok: false, present: [] }; });
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return Promise.resolve({ ok: false, present: [] });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Hear about installs, removals and activations; a Host without the event bus returns a no-op. */
|
|
331
|
+
function observePresence(ctx, listener) {
|
|
332
|
+
var remote = suiteServices(ctx).remote;
|
|
333
|
+
var on = safeGet(remote, "$on");
|
|
334
|
+
if (typeof on !== "function") return function () {};
|
|
335
|
+
try {
|
|
336
|
+
var off = on.call(remote, "plugin-manager/changed", listener);
|
|
337
|
+
return typeof off === "function" ? function () { off(); } : function () {};
|
|
338
|
+
} catch (e) {
|
|
339
|
+
return function () {};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The centre's main panel, where this Host registered one. A slots entry keeps its
|
|
345
|
+
* identity in `options` (`options.key` is the panel id the layout selects); older
|
|
346
|
+
* shapes kept `key`/`id` at the top level, which is still read as a fallback.
|
|
347
|
+
*/
|
|
348
|
+
function centerPanel(ctx) {
|
|
349
|
+
var slots = suiteServices(ctx).slots;
|
|
350
|
+
var entries = safeGet(slots, "entries");
|
|
351
|
+
if (typeof entries !== "function") return undefined;
|
|
352
|
+
var listed;
|
|
353
|
+
try { listed = entries.call(slots, "main"); } catch (e) { return undefined; }
|
|
354
|
+
if (!Array.isArray(listed)) return undefined;
|
|
355
|
+
for (var i = 0; i < listed.length; i++) {
|
|
356
|
+
var record = asRecord(listed[i]);
|
|
357
|
+
var options = asRecord(safeGet(record, "options"));
|
|
358
|
+
var key = options ? (safeGet(options, "key") !== undefined ? safeGet(options, "key") : safeGet(options, "id"))
|
|
359
|
+
: (record ? (safeGet(record, "key") !== undefined ? safeGet(record, "key") : safeGet(record, "id")) : undefined);
|
|
360
|
+
if (key === CENTER_PANEL) return CENTER_PANEL;
|
|
361
|
+
}
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Send the person to the centre, or to the Host's own way of installing it. A panel that
|
|
367
|
+
* is gone, a layout that refuses, and a missing navigation contract each fall through to
|
|
368
|
+
* the next, and the last answer is the spec a person can paste into the Plugins page.
|
|
369
|
+
*/
|
|
370
|
+
function openCenter(ctx) {
|
|
371
|
+
var services = suiteServices(ctx);
|
|
372
|
+
var panel = centerPanel(ctx);
|
|
373
|
+
var selectPanel = safeGet(services.layout, "selectPanel");
|
|
374
|
+
if (panel !== undefined && typeof selectPanel === "function") {
|
|
375
|
+
try { selectPanel.call(services.layout, panel); return { kind: "center" }; } catch (e) { /* the panel is gone */ }
|
|
376
|
+
}
|
|
377
|
+
var navigation = services.navigation;
|
|
378
|
+
var openBundle = safeGet(navigation, "openBundle");
|
|
379
|
+
if (typeof openBundle === "function") {
|
|
380
|
+
try { openBundle.call(navigation, SUITE.center); return { kind: "plugins" }; } catch (e) { /* no Plugins panel here */ }
|
|
381
|
+
}
|
|
382
|
+
var openInstall = safeGet(navigation, "openInstall");
|
|
383
|
+
if (typeof openInstall === "function") {
|
|
384
|
+
try { openInstall.call(navigation, { spec: CENTER_SPEC }); return { kind: "plugins" }; } catch (e) { /* nothing could open */ }
|
|
385
|
+
}
|
|
386
|
+
return { kind: "manual", spec: CENTER_SPEC };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Which of `wanted` this Host does not have yet, in the order the surface lists them. */
|
|
390
|
+
function missingFrom(present, wanted) {
|
|
391
|
+
return wanted.filter(function (want) { return present.indexOf(want.package) < 0; });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** What the card says: the absent components, or the one way to the rest. An unreadable inventory claims nothing. */
|
|
395
|
+
function suiteHint(read, wanted) {
|
|
396
|
+
if (!read.ok) return { kind: "tools", missing: [] };
|
|
397
|
+
var missing = missingFrom(read.present, wanted);
|
|
398
|
+
if (missing.length === 0) return { kind: "tools", missing: [] };
|
|
399
|
+
return {
|
|
400
|
+
kind: "missing",
|
|
401
|
+
missing: missing.map(function (want) { return { package: want.package, line: want.line }; }),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Write text through the browser's clipboard, where the page has one. */
|
|
406
|
+
function writeClipboard(text) {
|
|
407
|
+
var clipboard = typeof navigator !== "undefined" && navigator ? safeGet(navigator, "clipboard") : undefined;
|
|
408
|
+
var write = safeGet(clipboard, "writeText");
|
|
409
|
+
if (typeof write !== "function") return Promise.resolve(false);
|
|
410
|
+
try {
|
|
411
|
+
return Promise.resolve(write.call(clipboard, text)).then(function () { return true; }, function () { return false; });
|
|
412
|
+
} catch (e) {
|
|
413
|
+
return Promise.resolve(false);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** One read of what this Host has, shared by every card this bundle renders. */
|
|
418
|
+
function createSuiteStore(ctx, wanted) {
|
|
419
|
+
var snapshot = { read: false, found: { ok: false, present: [] }, hint: { kind: "tools", missing: [] } };
|
|
420
|
+
var listeners = [];
|
|
421
|
+
function publish(next) {
|
|
422
|
+
snapshot = next;
|
|
423
|
+
var copy = listeners.slice();
|
|
424
|
+
for (var i = 0; i < copy.length; i++) copy[i]();
|
|
425
|
+
}
|
|
426
|
+
function read() {
|
|
427
|
+
return readPresence(ctx).then(function (found) {
|
|
428
|
+
publish({ read: true, found: found, hint: suiteHint(found, wanted) });
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
subscribe: function (listener) {
|
|
433
|
+
listeners.push(listener);
|
|
434
|
+
return function () {
|
|
435
|
+
var at = listeners.indexOf(listener);
|
|
436
|
+
if (at >= 0) listeners.splice(at, 1);
|
|
437
|
+
};
|
|
438
|
+
},
|
|
439
|
+
getSnapshot: function () { return snapshot; },
|
|
440
|
+
open: function () { return openCenter(ctx); },
|
|
441
|
+
copy: writeClipboard,
|
|
442
|
+
/** Start the first read and every later refresh; the returned disposer releases both. */
|
|
443
|
+
start: function () {
|
|
444
|
+
void read();
|
|
445
|
+
return observePresence(ctx, function () { void read(); });
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The companion components this Host lacks, and the one quiet way into the plugin centre.
|
|
452
|
+
* It renders nothing until its own read settled, installs nothing, and opens nothing by
|
|
453
|
+
* itself: every line here is a statement about the Host, and the button is the only action.
|
|
454
|
+
*/
|
|
455
|
+
function SuiteCard(props) {
|
|
456
|
+
var store = useMemo(function () {
|
|
457
|
+
return props.store || createSuiteStore(props.ctx, COMPANIONS);
|
|
458
|
+
}, [props.ctx, props.store]);
|
|
459
|
+
var st = useState(store.getSnapshot()), snap = st[0], setSnap = st[1];
|
|
460
|
+
var mn = useState(null), manual = mn[0], setManual = mn[1];
|
|
461
|
+
var cp = useState(false), copied = cp[0], setCopied = cp[1];
|
|
462
|
+
|
|
463
|
+
useEffect(function () {
|
|
464
|
+
// Subscribe before the first read: a change published during it must still reach this card.
|
|
465
|
+
var off = store.subscribe(function () { setSnap(store.getSnapshot()); });
|
|
466
|
+
var stop = store.start();
|
|
467
|
+
return function () { off(); stop(); };
|
|
468
|
+
}, [store]);
|
|
469
|
+
|
|
470
|
+
if (!snap.read) return null;
|
|
471
|
+
var missing = snap.hint.kind === "missing";
|
|
472
|
+
var kids = [
|
|
473
|
+
h("div", { key: "t", style: { fontSize: "12px", fontWeight: 600 } }, missing ? SUITE_COPY.missingTitle : SUITE_COPY.toolsTitle),
|
|
474
|
+
h("div", { key: "i", style: { fontSize: "11px", color: C.muted, lineHeight: 1.6 } }, missing ? SUITE_COPY.missingIntro : SUITE_COPY.toolsIntro),
|
|
475
|
+
];
|
|
476
|
+
for (var i = 0; i < snap.hint.missing.length; i++) {
|
|
477
|
+
var line = snap.hint.missing[i];
|
|
478
|
+
kids.push(h("div", { key: "m:" + line.package, style: { fontSize: "12px", lineHeight: 1.6 } }, line.line));
|
|
479
|
+
}
|
|
480
|
+
kids.push(h("div", { key: "k", style: { fontSize: "11px", color: C.muted, lineHeight: 1.6 } }, SUITE_COPY.keep));
|
|
481
|
+
kids.push(h("div", { key: "a", style: { display: "flex", flexWrap: "wrap", gap: "6px", paddingTop: "2px" } }, [
|
|
482
|
+
h(Btn, {
|
|
483
|
+
key: "open", active: missing, title: "打开插件中心",
|
|
484
|
+
onClick: function () {
|
|
485
|
+
var target = store.open();
|
|
486
|
+
setCopied(false);
|
|
487
|
+
setManual(target.kind === "manual" ? { spec: target.spec } : null);
|
|
488
|
+
},
|
|
489
|
+
}, SUITE_COPY.open),
|
|
490
|
+
]));
|
|
491
|
+
if (manual) {
|
|
492
|
+
kids.push(h("div", { key: "mn", style: { fontSize: "11px", color: C.muted, lineHeight: 1.6 } }, SUITE_COPY.manual));
|
|
493
|
+
kids.push(h("code", {
|
|
494
|
+
key: "spec",
|
|
495
|
+
style: { fontSize: "12px", padding: "2px 6px", borderRadius: "6px", background: C.softer, border: "1px solid " + C.line, userSelect: "text", alignSelf: "flex-start" },
|
|
496
|
+
}, manual.spec));
|
|
497
|
+
kids.push(h("div", { key: "cp", style: { display: "flex", flexWrap: "wrap", gap: "6px" } }, [
|
|
498
|
+
h(Btn, {
|
|
499
|
+
key: "copy", title: "复制包名",
|
|
500
|
+
onClick: function () { void store.copy(manual.spec).then(function (written) { setCopied(written); }); },
|
|
501
|
+
}, copied ? SUITE_COPY.copied : SUITE_COPY.copy),
|
|
502
|
+
]));
|
|
503
|
+
}
|
|
504
|
+
return h("div", {
|
|
505
|
+
"data-mp-companions": "1",
|
|
506
|
+
style: {
|
|
507
|
+
display: "flex", flexDirection: "column", gap: "5px", flex: "none", boxSizing: "border-box",
|
|
508
|
+
padding: "9px 10px", borderRadius: "8px", border: "1px dashed " + C.line, background: C.softer, color: C.fg,
|
|
509
|
+
},
|
|
510
|
+
}, kids);
|
|
199
511
|
}
|
|
200
512
|
|
|
201
513
|
// ── video ───────────────────────────────────────────────────────────────
|
|
@@ -209,13 +521,14 @@ window.__ModuleLoader__.load({
|
|
|
209
521
|
var er = useState(null), err = er[0], setErr = er[1];
|
|
210
522
|
var du = useState(null), dur = du[0], setDur = du[1];
|
|
211
523
|
var dims = useState(null), dim = dims[0], setDim = dims[1];
|
|
524
|
+
var tl = useState(false), tools = tl[0], setTools = tl[1];
|
|
212
525
|
|
|
213
526
|
useEffect(function () { setErr(null); setDur(null); setDim(null); }, [url]);
|
|
214
527
|
useEffect(function () { if (vref.current) vref.current.playbackRate = rate; }, [rate, url]);
|
|
215
528
|
|
|
216
529
|
var info = [dur != null ? fmtDur(dur) : null, dim ? dim : null, extOf(abs).toUpperCase()].filter(Boolean).join(" · ");
|
|
217
530
|
|
|
218
|
-
return h(Shell, null, [
|
|
531
|
+
return h(Shell, { key: "root", tools: tools ? h(SuiteCard, { ctx: props.ctx }) : null }, [
|
|
219
532
|
h("div", { key: "stage", style: { flex: "1 1 auto", minHeight: 0, display: "flex", background: "#000", borderRadius: "8px", overflow: "hidden" } },
|
|
220
533
|
err
|
|
221
534
|
? h("div", { style: { margin: "auto", padding: "20px", textAlign: "center", color: "#ddd", fontSize: "13px", lineHeight: 1.7 } }, [
|
|
@@ -238,7 +551,7 @@ window.__ModuleLoader__.load({
|
|
|
238
551
|
h(Bar, { key: "bar", title: baseName(abs) + (info ? " · " + info : ""), path: abs }, [
|
|
239
552
|
h(RatePicker, { key: "rate", rate: rate, set: setRate }),
|
|
240
553
|
h(Btn, { key: "loop", active: loop, title: "循环播放", onClick: function () { setLoop(!loop); } }, "循环"),
|
|
241
|
-
CommonActions(props, abs, url),
|
|
554
|
+
CommonActions(props, abs, url, tools, function () { setTools(!tools); }),
|
|
242
555
|
]),
|
|
243
556
|
]);
|
|
244
557
|
}
|
|
@@ -254,6 +567,7 @@ window.__ModuleLoader__.load({
|
|
|
254
567
|
var lp = useState(false), loop = lp[0], setLoop = lp[1];
|
|
255
568
|
var er = useState(null), err = er[0], setErr = er[1];
|
|
256
569
|
var du = useState(null), dur = du[0], setDur = du[1];
|
|
570
|
+
var tl = useState(false), tools = tl[0], setTools = tl[1];
|
|
257
571
|
|
|
258
572
|
useEffect(function () { setErr(null); setDur(null); }, [url]);
|
|
259
573
|
useEffect(function () { if (aref.current) aref.current.playbackRate = rate; }, [rate, url]);
|
|
@@ -304,7 +618,7 @@ window.__ModuleLoader__.load({
|
|
|
304
618
|
};
|
|
305
619
|
}, [url]);
|
|
306
620
|
|
|
307
|
-
return h(Shell, null, [
|
|
621
|
+
return h(Shell, { key: "root", tools: tools ? h(SuiteCard, { ctx: props.ctx }) : null }, [
|
|
308
622
|
h("div", {
|
|
309
623
|
key: "stage",
|
|
310
624
|
style: {
|
|
@@ -326,7 +640,7 @@ window.__ModuleLoader__.load({
|
|
|
326
640
|
h(Bar, { key: "bar", title: baseName(abs) + " · " + [dur != null ? fmtDur(dur) : null, extOf(abs).toUpperCase()].filter(Boolean).join(" · "), path: abs }, [
|
|
327
641
|
h(RatePicker, { key: "rate", rate: rate, set: setRate }),
|
|
328
642
|
h(Btn, { key: "loop", active: loop, title: "循环播放", onClick: function () { setLoop(!loop); } }, "循环"),
|
|
329
|
-
CommonActions(props, abs, url),
|
|
643
|
+
CommonActions(props, abs, url, tools, function () { setTools(!tools); }),
|
|
330
644
|
]),
|
|
331
645
|
]);
|
|
332
646
|
}
|
|
@@ -348,6 +662,7 @@ window.__ModuleLoader__.load({
|
|
|
348
662
|
var vp = useState("fit"), view = vp[0], setView = vp[1];
|
|
349
663
|
var rk = useState(0), reloadKey = rk[0], setReloadKey = rk[1];
|
|
350
664
|
var sr = useState({ state: "idle", text: "" }), source = sr[0], setSource = sr[1];
|
|
665
|
+
var tl = useState(false), tools = tl[0], setTools = tl[1];
|
|
351
666
|
|
|
352
667
|
var frameRef = useRef(null);
|
|
353
668
|
|
|
@@ -397,7 +712,7 @@ window.__ModuleLoader__.load({
|
|
|
397
712
|
}));
|
|
398
713
|
}
|
|
399
714
|
|
|
400
|
-
return h(Shell, null, [
|
|
715
|
+
return h(Shell, { key: "root", tools: tools ? h(SuiteCard, { ctx: props.ctx }) : null }, [
|
|
401
716
|
stage,
|
|
402
717
|
h(Bar, { key: "bar", title: baseName(abs), path: abs }, [
|
|
403
718
|
h(Btn, { key: "m1", active: mode === "render", title: "渲染预览", onClick: function () { setMode("render"); } }, "预览"),
|
|
@@ -407,7 +722,7 @@ window.__ModuleLoader__.load({
|
|
|
407
722
|
}) : null,
|
|
408
723
|
h(Btn, { key: "js", active: scripts, title: scripts ? "脚本已允许,点击关闭(页面将重新加载)" : "脚本已禁用,点击允许", onClick: function () { setScripts(!scripts); } }, scripts ? "脚本 开" : "脚本 关"),
|
|
409
724
|
h(Btn, { key: "rl", title: "重新加载", onClick: function () { setReloadKey(reloadKey + 1); } }, "刷新"),
|
|
410
|
-
CommonActions(props, abs, url),
|
|
725
|
+
CommonActions(props, abs, url, tools, function () { setTools(!tools); }),
|
|
411
726
|
]),
|
|
412
727
|
]);
|
|
413
728
|
}
|
|
@@ -469,7 +784,7 @@ window.__ModuleLoader__.load({
|
|
|
469
784
|
|
|
470
785
|
var nav = { position: "absolute", top: "50%", transform: "translateY(-50%)", width: "30px", height: "56px", border: "none", borderRadius: "6px", background: "rgba(127,127,127,.35)", color: "inherit", cursor: "pointer", fontSize: "18px", zIndex: 2 };
|
|
471
786
|
return h("div", {
|
|
472
|
-
style: { position: "absolute", inset: 0, zIndex: 20, display: "flex", flexDirection: "column", background:
|
|
787
|
+
style: { position: "absolute", inset: 0, zIndex: 20, display: "flex", flexDirection: "column", background: C.bg, color: C.fg },
|
|
473
788
|
}, [
|
|
474
789
|
h("div", { key: "top", style: { display: "flex", alignItems: "center", gap: "8px", padding: "8px 10px", borderBottom: "1px solid " + C.line, fontSize: "12px", flex: "none" } }, [
|
|
475
790
|
h(Btn, { key: "x", title: "返回画廊 (Esc)", onClick: p.onClose }, "← 返回"),
|
|
@@ -627,6 +942,8 @@ window.__ModuleLoader__.load({
|
|
|
627
942
|
return h("div", { style: { position: "relative", height: "100%", display: "flex", flexDirection: "column", overflow: "hidden", minWidth: 0 } }, [
|
|
628
943
|
header,
|
|
629
944
|
body,
|
|
945
|
+
// The card sits at the foot of the gallery: a statement about the Host, never a launch ad.
|
|
946
|
+
h("div", { key: "tools", style: { flex: "none", padding: "0 10px 10px" } }, h(SuiteCard, { ctx: props.ctx })),
|
|
630
947
|
sel >= 0 && items[sel]
|
|
631
948
|
? h(Lightbox, {
|
|
632
949
|
key: "lb", ctx: props.ctx, store: props.store, scope: scope, item: items[sel], index: sel, total: items.length,
|
|
@@ -674,7 +991,19 @@ window.__ModuleLoader__.load({
|
|
|
674
991
|
});
|
|
675
992
|
}
|
|
676
993
|
|
|
677
|
-
|
|
994
|
+
// The loader reads `inject` and `apply`; `__suite` is the companion-components core, kept
|
|
995
|
+
// exported so test/suite.test.mjs can drive it and the card through a fake loader and React.
|
|
996
|
+
module.exports = {
|
|
997
|
+
inject: inject,
|
|
998
|
+
apply: apply,
|
|
999
|
+
__suite: {
|
|
1000
|
+
SUITE: SUITE, CENTER_PANEL: CENTER_PANEL, CENTER_SPEC: CENTER_SPEC, COMPANIONS: COMPANIONS,
|
|
1001
|
+
suiteServices: suiteServices, bundlePresent: bundlePresent, presentFrom: presentFrom,
|
|
1002
|
+
readPresence: readPresence, observePresence: observePresence, centerPanel: centerPanel,
|
|
1003
|
+
openCenter: openCenter, missingFrom: missingFrom, suiteHint: suiteHint,
|
|
1004
|
+
createSuiteStore: createSuiteStore, SuiteCard: SuiteCard,
|
|
1005
|
+
},
|
|
1006
|
+
};
|
|
678
1007
|
return module.exports;
|
|
679
1008
|
},
|
|
680
1009
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibedev-si/dsh-media-viewer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "右侧栏媒体预览:直接预览视频、音频和 HTML(大文件可拖进度条,HTML 默认允许脚本),并提供文件夹媒体画廊,需配合 dsh-better-sidebar · Inline video, audio and HTML preview plus a folder media gallery in the dsh-better-sidebar right column — range-streamed large files, sandboxed HTML with working relative assets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"registry": "https://registry.npmjs.org/"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
|
-
"test": "node test/route.test.mjs"
|
|
56
|
+
"test": "node test/route.test.mjs && node test/tokens.test.mjs && node test/suite.test.mjs"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"dsh-better-sidebar": ">=0.24.0"
|