dsh-plugin-ekko-skin-manager 0.2.2 → 0.3.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/README.md +5 -5
- package/lib/client.js +5 -2
- package/lib/index.js +13 -4
- package/package.json +1 -1
- package/src/client.jsx +1 -1
- package/src/controller.mjs +2 -0
- package/src/index.mjs +12 -4
- package/src/video-settings.jsx +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ekko 皮肤管理器 · 旧版兼容入口
|
|
2
2
|
|
|
3
|
-
**主题管理功能已整合到新版主题中。** 新用户请直接安装 [
|
|
3
|
+
**主题管理功能已整合到新版主题中。** 新用户请直接安装 [初音未来与玫瑰女王主题套装](https://github.com/cjjchina3088191870-sudo/dsh-plugin-miku-theme),主题内已包含管理界面、手机导航、外观设置和启动视频功能。
|
|
4
4
|
|
|
5
5
|
本仓库保留旧版兼容与迁移说明。新版主题可独立使用,安装其他兼容主题后,会自动纳入同一管理界面。
|
|
6
6
|
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
适配 DSH 0.2.2-rc.1:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
dsh plugin --profile web add
|
|
12
|
+
dsh plugin --profile web add dsh-plugin-miku-theme
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
重启 DSH 并刷新网页,进入 **账号菜单 → 设置 → Ekko 皮肤** 即可使用。
|
|
16
16
|
|
|
17
|
-
电脑与手机可以分别上传启动视频,支持 MP4 / WebM、预览、声音控制与不同画面比例。完整介绍和主题预览请查看 [
|
|
17
|
+
电脑与手机可以分别上传启动视频,支持 MP4 / WebM、预览、声音控制与不同画面比例。完整介绍和主题预览请查看 [初音未来与玫瑰女王主题套装仓库](https://github.com/cjjchina3088191870-sudo/dsh-plugin-miku-theme#readme)。
|
|
18
18
|
|
|
19
19
|
## 已安装独立管理器的用户
|
|
20
20
|
|
|
@@ -22,12 +22,12 @@ dsh plugin --profile web add github:cjjchina3088191870-sudo/dsh-plugin-miku-them
|
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
24
|
dsh plugin --profile web remove dsh-plugin-ekko-skin-manager
|
|
25
|
-
dsh plugin --profile web add
|
|
25
|
+
dsh plugin --profile web add dsh-plugin-miku-theme
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
完成后重启 DSH 并刷新网页。主题偏好和已保存的视频会保留;使用其他 profile 时,将 `web` 替换为对应名称。
|
|
29
29
|
|
|
30
|
-
独立管理器 0.
|
|
30
|
+
独立管理器 0.3.x 与新版主题套装可以共用管理界面,但日常使用只安装主题即可。
|
|
31
31
|
|
|
32
32
|
## 许可
|
|
33
33
|
|
package/lib/client.js
CHANGED
|
@@ -379,7 +379,7 @@ function VideoSettings({ skin }) {
|
|
|
379
379
|
};
|
|
380
380
|
if (!data || !draft) return /* @__PURE__ */ import_react.default.createElement("section", { className: "ekko-skin-options ekko-video-settings", "data-testid": "ekko-video-settings" }, /* @__PURE__ */ import_react.default.createElement("h3", null, "\u542F\u52A8\u89C6\u9891"), /* @__PURE__ */ import_react.default.createElement("p", { role: "status" }, message || "\u6B63\u5728\u8BFB\u53D6\u89C6\u9891\u8BBE\u7F6E\u2026"), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", onClick: load }, "\u91CD\u8BD5"));
|
|
381
381
|
const update = (patch) => setDraft({ ...draft, ...patch }), fitKey = device === "mobile" ? "mobileFit" : "fit";
|
|
382
|
-
return /* @__PURE__ */ import_react.default.createElement("section", { className: "ekko-skin-options ekko-video-settings", "data-testid": "ekko-video-settings", "data-device": device }, /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-skin-toolbar" }, /* @__PURE__ */ import_react.default.createElement("h3", null, skin === "miku" ? "\u521D\u97F3\u672A\u6765" : "\
|
|
382
|
+
return /* @__PURE__ */ import_react.default.createElement("section", { className: "ekko-skin-options ekko-video-settings", "data-testid": "ekko-video-settings", "data-device": device }, /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-skin-toolbar" }, /* @__PURE__ */ import_react.default.createElement("h3", null, skin === "miku" ? "\u521D\u97F3\u672A\u6765" : "\u73AB\u7470\u5973\u738B", " \xB7 \u542F\u52A8\u89C6\u9891"), /* @__PURE__ */ import_react.default.createElement("span", { className: "ekko-skin-version" }, "\u6BCF\u6B3E\u4E3B\u9898\u5206\u522B\u4FDD\u5B58")), /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-video-controls" }, /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react.default.createElement("span", null, "\u542F\u7528\u542F\u52A8\u89C6\u9891", /* @__PURE__ */ import_react.default.createElement("small", null, "\u9875\u9762\u542F\u52A8\u65F6\u64AD\u653E\uFF0C\u53EF\u968F\u65F6\u8DF3\u8FC7")), /* @__PURE__ */ import_react.default.createElement("input", { type: "checkbox", checked: draft.enabled, disabled: busy, onChange: (e) => update({ enabled: e.target.checked }) })), /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react.default.createElement("span", null, "\u64AD\u653E\u58F0\u97F3", /* @__PURE__ */ import_react.default.createElement("small", null, "\u6D4F\u89C8\u5668\u9650\u5236\u81EA\u52A8\u64AD\u653E\u65F6\u5148\u9759\u97F3\uFF0C\u70B9\u51FB\u53EF\u5F00\u542F\u58F0\u97F3")), /* @__PURE__ */ import_react.default.createElement("input", { type: "checkbox", checked: draft.sound, disabled: busy, onChange: (e) => update({ sound: e.target.checked }) })), /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react.default.createElement("span", null, "\u97F3\u91CF", /* @__PURE__ */ import_react.default.createElement("small", null, Math.round(draft.volume * 100), "%")), /* @__PURE__ */ import_react.default.createElement("input", { type: "range", min: "0", max: "100", value: Math.round(draft.volume * 100), disabled: busy, onChange: (e) => update({ volume: Number(e.target.value) / 100 }) })), /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react.default.createElement("span", null, "\u6700\u957F\u64AD\u653E\u65F6\u95F4", /* @__PURE__ */ import_react.default.createElement("small", null, "5\u201360 \u79D2\uFF0C\u5230\u65F6\u81EA\u52A8\u8FDB\u5165\u754C\u9762")), /* @__PURE__ */ import_react.default.createElement("input", { className: "ekko-video-seconds", type: "number", min: "5", max: "60", value: draft.maxSeconds, disabled: busy, onChange: (e) => update({ maxSeconds: Number(e.target.value) }) }))), /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-video-device-tabs", role: "tablist", "aria-label": "\u542F\u52A8\u89C6\u9891\u5C4F\u5E55\u7C7B\u578B" }, ["desktop", "mobile"].map((value) => /* @__PURE__ */ import_react.default.createElement("button", { key: value, type: "button", role: "tab", id: "ekko-video-tab-" + skin + "-" + value, "aria-selected": device === value, "aria-controls": "ekko-video-panel-" + skin, disabled: busy, className: "ekko-skin-button", "data-active": device === value, onClick: () => {
|
|
383
383
|
setDevice(value);
|
|
384
384
|
setMessage("");
|
|
385
385
|
} }, value === "mobile" ? "\u624B\u673A\u7AEF\u89C6\u9891" : "\u7535\u8111\u7AEF\u89C6\u9891"))), /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-video-device-panel", role: "tabpanel", id: "ekko-video-panel-" + skin, "aria-labelledby": "ekko-video-tab-" + skin + "-" + device }, /* @__PURE__ */ import_react.default.createElement("p", { className: "ekko-skin-version" }, device === "mobile" ? "\u624B\u673A\u53CA\u7A84\u5C4F\u81EA\u52A8\u4F7F\u7528\u6B64\u89C6\u9891\uFF0C\u63A8\u8350 9:16 \u7AD6\u5C4F\uFF1B\u672A\u4E0A\u4F20\u65F6\u6CBF\u7528\u7535\u8111\u7AEF\u89C6\u9891\u3002" : "\u7535\u8111\u53CA\u5BBD\u5C4F\u4F7F\u7528\u6B64\u89C6\u9891\uFF0C\u63A8\u8350 16:9 \u6A2A\u5C4F\u3002", "\u652F\u6301\u5176\u4ED6\u5206\u8FA8\u7387\u4E0E\u6BD4\u4F8B\u3002"), /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react.default.createElement("span", null, device === "mobile" ? "\u624B\u673A\u7AEF" : "\u7535\u8111\u7AEF", "\u753B\u9762\u663E\u793A"), /* @__PURE__ */ import_react.default.createElement("select", { "aria-label": (device === "mobile" ? "\u624B\u673A\u7AEF" : "\u7535\u8111\u7AEF") + "\u753B\u9762\u663E\u793A", value: draft[fitKey], disabled: busy, onChange: (e) => update({ [fitKey]: e.target.value }) }, /* @__PURE__ */ import_react.default.createElement("option", { value: "cover" }, "\u94FA\u6EE1\u5C4F\u5E55"), /* @__PURE__ */ import_react.default.createElement("option", { value: "contain" }, "\u5B8C\u6574\u663E\u793A"))), /* @__PURE__ */ import_react.default.createElement("video", { key: device + source, ref: preview, className: "ekko-video-preview", "data-device": device, muted: !draft.sound, controls: true, playsInline: true, preload: "metadata", onLoadedMetadata: () => setMediaError(""), onError: (e) => {
|
|
@@ -475,6 +475,9 @@ function createController({ storage, mount, prepare = async () => {
|
|
|
475
475
|
},
|
|
476
476
|
register(provider) {
|
|
477
477
|
if (!["rose", "miku"].includes(provider.id) || typeof provider.plugin?.apply !== "function") throw new TypeError("Unknown Ekko skin provider");
|
|
478
|
+
const existing = providers.get(provider.id);
|
|
479
|
+
if (existing && (existing.priority || 0) > (provider.priority || 0)) return () => {
|
|
480
|
+
};
|
|
478
481
|
providers.set(provider.id, provider);
|
|
479
482
|
publish({});
|
|
480
483
|
if (initialized && provider.id === wanted) void reconcile();
|
|
@@ -705,7 +708,7 @@ function createMobileNavigation({ layout, document: doc = document }) {
|
|
|
705
708
|
|
|
706
709
|
// src/client.jsx
|
|
707
710
|
var inject = ["slots", "theme", "layout"];
|
|
708
|
-
var NAMES = { rose: { zh: "\
|
|
711
|
+
var NAMES = { rose: { zh: "\u73AB\u7470\u5973\u738B", en: "Rose Queen" }, miku: { zh: "\u521D\u97F3\u672A\u6765", en: "Hatsune Miku" } };
|
|
709
712
|
var COPY = {
|
|
710
713
|
zh: { nav: "Ekko \u76AE\u80A4", title: "\u73AB\u7470\u4E0E\u521D\u97F3", intro: "\u9009\u62E9\u4F60\u7684\u521B\u4F5C\u7A7A\u95F4\uFF1B\u6BCF\u4E2A\u4E3B\u9898\u7684\u88C5\u9970\u504F\u597D\u5206\u522B\u4FDD\u7559\u3002", use: "\u4F7F\u7528", active: "\u5F53\u524D\u4F7F\u7528", switching: "\u6B63\u5728\u5207\u6362\u2026", native: "\u539F\u751F\u754C\u9762", options: "\u4E3B\u9898\u88C5\u9970", artwork: "\u6B22\u8FCE\u9875\u88C5\u9970", companion: "\u5BF9\u8BDD\u4EBA\u7269\u80CC\u666F", polaroid: "\u53F3\u4E0B\u89D2\u7167\u7247", opacity: "\u5BF9\u8BDD\u80CC\u666F\u6D53\u5EA6", reset: "\u6062\u590D\u6B64\u4E3B\u9898\u9ED8\u8BA4", loading: "\u6B63\u5728\u52A0\u8F7D\u4E3B\u9898\u2026", unavailable: "\u53EF\u9009\u4E3B\u9898\uFF0C\u5C1A\u672A\u5B89\u88C5", retry: "\u91CD\u8BD5", loadFailed: "\u76AE\u80A4\u7BA1\u7406\u5668\u6682\u65F6\u65E0\u6CD5\u52A0\u8F7D\uFF0C\u8BF7\u91CD\u8BD5\u3002", materialFailed: "\u4E3B\u9898\u7D20\u6750\u672A\u80FD\u52A0\u8F7D\uFF0C\u8BF7\u91CD\u8BD5\u3002" },
|
|
711
714
|
en: { nav: "Ekko skins", title: "Rose & Miku", intro: "Choose your creative space. Each skin keeps its own appearance preferences.", use: "Use", active: "In use", switching: "Switching\u2026", native: "Native appearance", options: "Artwork preferences", artwork: "Welcome artwork", companion: "Chat character background", polaroid: "Corner photo", opacity: "Chat background opacity", reset: "Reset this skin", loading: "Loading skins\u2026", unavailable: "Optional skin, not installed", retry: "Retry", loadFailed: "Unable to load the skin manager. Please retry.", materialFailed: "Unable to load the skin artwork. Please retry." }
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/index.mjs
|
|
2
2
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
3
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { createHash as createHash2 } from "node:crypto";
|
|
5
6
|
import { dirname, join as join2 } from "node:path";
|
|
@@ -330,12 +331,20 @@ var BOOT_SCRIPT = '(()=>{var D=`\n.ekko-boot{position:fixed;inset:0;z-index:2147
|
|
|
330
331
|
var require2 = createRequire(import.meta.url);
|
|
331
332
|
var PREFIX = "/ekko-skins";
|
|
332
333
|
var PACKAGES = { rose: "dsh-plugin-rose-theme", miku: "dsh-plugin-miku-theme" };
|
|
334
|
+
function resolveSkinPackage(resolvePackage, id) {
|
|
335
|
+
if (id === "rose") try {
|
|
336
|
+
const bundled = join2(resolvePackage(PACKAGES.miku), "themes/rose");
|
|
337
|
+
if (existsSync2(join2(bundled, "package.json"))) return bundled;
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
return resolvePackage(PACKAGES[id]);
|
|
341
|
+
}
|
|
333
342
|
function createHandler({ resolvePackage = (name) => dirname(require2.resolve(name + "/package.json")), defaultSkin = "miku" } = {}) {
|
|
334
343
|
async function catalog() {
|
|
335
344
|
const skins = [];
|
|
336
345
|
for (const [id, pkg] of Object.entries(PACKAGES)) {
|
|
337
346
|
try {
|
|
338
|
-
const root = resolvePackage
|
|
347
|
+
const root = resolveSkinPackage(resolvePackage, id), meta = JSON.parse(await readFile2(join2(root, "package.json"), "utf8"));
|
|
339
348
|
skins.push({ id, version: meta.version, available: meta.dsh?.ekkoSkinProvider === 1, preview: { light: "./ekko-skins/preview/" + id + "/light.webp", dark: "./ekko-skins/preview/" + id + "/dark.webp" } });
|
|
340
349
|
} catch {
|
|
341
350
|
skins.push({ id, available: false });
|
|
@@ -344,7 +353,7 @@ function createHandler({ resolvePackage = (name) => dirname(require2.resolve(nam
|
|
|
344
353
|
const installed = skins.filter((s) => s.available).map((s) => s.id);
|
|
345
354
|
const preferred = ["rose", "miku", "default"].includes(defaultSkin) ? defaultSkin : "miku";
|
|
346
355
|
const selected = preferred === "default" || installed.includes(preferred) ? preferred : installed[0] || "default";
|
|
347
|
-
return { version: "0.
|
|
356
|
+
return { version: "0.3.0", defaultSkin: selected, skins };
|
|
348
357
|
}
|
|
349
358
|
return async (req, res) => {
|
|
350
359
|
if (!["GET", "HEAD"].includes(req.method)) {
|
|
@@ -366,7 +375,7 @@ function createHandler({ resolvePackage = (name) => dirname(require2.resolve(nam
|
|
|
366
375
|
const [, kind, id, file] = match;
|
|
367
376
|
let bytes;
|
|
368
377
|
try {
|
|
369
|
-
const root = resolvePackage
|
|
378
|
+
const root = resolveSkinPackage(resolvePackage, id);
|
|
370
379
|
if (kind === "assets") {
|
|
371
380
|
if (!/^[a-f0-9]{64}\.(webp|png|svg|woff2)$/.test(file)) {
|
|
372
381
|
res.writeHead(404).end();
|
|
@@ -405,7 +414,7 @@ function apply(ctx, config = {}) {
|
|
|
405
414
|
ctx.inject(["webServer", "profileContext", "connection"], async (child) => {
|
|
406
415
|
const profile = child.get("profileContext"), resolver = createRequire(pathToFileURL(join2(profile.dir, "package.json")));
|
|
407
416
|
const resolvePackage = (name) => dirname(resolver.resolve(name + "/package.json"));
|
|
408
|
-
const media = await createMediaStore({ directory: join2(profile.dir, "ekko-skins-media"), resolvePackage: (id) => resolvePackage
|
|
417
|
+
const media = await createMediaStore({ directory: join2(profile.dir, "ekko-skins-media"), resolvePackage: (id) => resolveSkinPackage(resolvePackage, id) });
|
|
409
418
|
child.effect(() => () => media.close(), "ekko-skins: private media store");
|
|
410
419
|
child.effect(() => child.get("webServer").register({ kind: "prefix", path: PREFIX, handler: createHandler({ defaultSkin: config.defaultSkin, resolvePackage }) }));
|
|
411
420
|
for (const [path, methods, fetch, requestBody] of [
|
package/package.json
CHANGED
package/src/client.jsx
CHANGED
|
@@ -4,7 +4,7 @@ import{createController,readSelection,SELECTION_KEY}from'./controller.mjs';
|
|
|
4
4
|
import{STYLES}from'./generated.mjs';
|
|
5
5
|
import{createMobileNavigation}from'./mobile-navigation.mjs';
|
|
6
6
|
export const inject=['slots','theme','layout'];
|
|
7
|
-
const NAMES={rose:{zh:'
|
|
7
|
+
const NAMES={rose:{zh:'玫瑰女王',en:'Rose Queen'},miku:{zh:'初音未来',en:'Hatsune Miku'}};
|
|
8
8
|
const COPY={
|
|
9
9
|
zh:{nav:'Ekko 皮肤',title:'玫瑰与初音',intro:'选择你的创作空间;每个主题的装饰偏好分别保留。',use:'使用',active:'当前使用',switching:'正在切换…',native:'原生界面',options:'主题装饰',artwork:'欢迎页装饰',companion:'对话人物背景',polaroid:'右下角照片',opacity:'对话背景浓度',reset:'恢复此主题默认',loading:'正在加载主题…',unavailable:'可选主题,尚未安装',retry:'重试',loadFailed:'皮肤管理器暂时无法加载,请重试。',materialFailed:'主题素材未能加载,请重试。'},
|
|
10
10
|
en:{nav:'Ekko skins',title:'Rose & Miku',intro:'Choose your creative space. Each skin keeps its own appearance preferences.',use:'Use',active:'In use',switching:'Switching…',native:'Native appearance',options:'Artwork preferences',artwork:'Welcome artwork',companion:'Chat character background',polaroid:'Corner photo',opacity:'Chat background opacity',reset:'Reset this skin',loading:'Loading skins…',unavailable:'Optional skin, not installed',retry:'Retry',loadFailed:'Unable to load the skin manager. Please retry.',materialFailed:'Unable to load the skin artwork. Please retry.'}
|
package/src/controller.mjs
CHANGED
|
@@ -48,6 +48,8 @@ export function createController({storage,mount,prepare=async()=>{},fallback='ro
|
|
|
48
48
|
subscribe(fn){listeners.add(fn);return()=>listeners.delete(fn);},
|
|
49
49
|
register(provider){
|
|
50
50
|
if(!['rose','miku'].includes(provider.id)||typeof provider.plugin?.apply!=='function')throw new TypeError('Unknown Ekko skin provider');
|
|
51
|
+
const existing=providers.get(provider.id);
|
|
52
|
+
if(existing&&(existing.priority||0)>(provider.priority||0))return()=>{};
|
|
51
53
|
providers.set(provider.id,provider);publish({});
|
|
52
54
|
if(initialized&&provider.id===wanted)void reconcile();
|
|
53
55
|
return()=>{if(providers.get(provider.id)!==provider)return;providers.delete(provider.id);publish({});if(provider.id===wanted||runtime?.provider===provider)void reconcile();};
|
package/src/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import {existsSync} from 'node:fs';
|
|
2
3
|
import {createRequire} from 'node:module';
|
|
3
4
|
import {createHash} from 'node:crypto';
|
|
4
5
|
import {dirname,join} from 'node:path';
|
|
@@ -9,15 +10,22 @@ import {toInlineJson} from './vendor/video-route.mjs';
|
|
|
9
10
|
const require=createRequire(import.meta.url);
|
|
10
11
|
export const PREFIX='/ekko-skins';
|
|
11
12
|
const PACKAGES={rose:'dsh-plugin-rose-theme',miku:'dsh-plugin-miku-theme'};
|
|
13
|
+
export function resolveSkinPackage(resolvePackage,id){
|
|
14
|
+
if(id==='rose')try{
|
|
15
|
+
const bundled=join(resolvePackage(PACKAGES.miku),'themes/rose');
|
|
16
|
+
if(existsSync(join(bundled,'package.json')))return bundled;
|
|
17
|
+
}catch{}
|
|
18
|
+
return resolvePackage(PACKAGES[id]);
|
|
19
|
+
}
|
|
12
20
|
export function createHandler({resolvePackage=name=>dirname(require.resolve(name+'/package.json')),defaultSkin='miku'}={}){
|
|
13
21
|
async function catalog(){
|
|
14
22
|
const skins=[];for(const[id,pkg]of Object.entries(PACKAGES)){
|
|
15
|
-
try{const root=resolvePackage
|
|
23
|
+
try{const root=resolveSkinPackage(resolvePackage,id),meta=JSON.parse(await readFile(join(root,'package.json'),'utf8'));skins.push({id,version:meta.version,available:meta.dsh?.ekkoSkinProvider===1,preview:{light:'./ekko-skins/preview/'+id+'/light.webp',dark:'./ekko-skins/preview/'+id+'/dark.webp'}});}catch{skins.push({id,available:false});}
|
|
16
24
|
}
|
|
17
25
|
const installed=skins.filter(s=>s.available).map(s=>s.id);
|
|
18
26
|
const preferred=['rose','miku','default'].includes(defaultSkin)?defaultSkin:'miku';
|
|
19
27
|
const selected=preferred==='default'||installed.includes(preferred)?preferred:(installed[0]||'default');
|
|
20
|
-
return{version:'0.
|
|
28
|
+
return{version:'0.3.0',defaultSkin:selected,skins};
|
|
21
29
|
}
|
|
22
30
|
return async(req,res)=>{
|
|
23
31
|
if(!['GET','HEAD'].includes(req.method)){res.writeHead(405,{Allow:'GET, HEAD'}).end();return;}
|
|
@@ -29,7 +37,7 @@ export function createHandler({resolvePackage=name=>dirname(require.resolve(name
|
|
|
29
37
|
if(!match){res.writeHead(404).end();return;}
|
|
30
38
|
const[,kind,id,file]=match;let bytes;
|
|
31
39
|
try{
|
|
32
|
-
const root=resolvePackage
|
|
40
|
+
const root=resolveSkinPackage(resolvePackage,id);
|
|
33
41
|
if(kind==='assets'){
|
|
34
42
|
if(!/^[a-f0-9]{64}\.(webp|png|svg|woff2)$/.test(file)){res.writeHead(404).end();return;}
|
|
35
43
|
// Read the current allowlist so updating artwork does not need a host restart.
|
|
@@ -55,7 +63,7 @@ export function apply(ctx,config={}){
|
|
|
55
63
|
ctx.inject(['webServer','profileContext','connection'],async child=>{
|
|
56
64
|
const profile=child.get('profileContext'),resolver=createRequire(pathToFileURL(join(profile.dir,'package.json')));
|
|
57
65
|
const resolvePackage=name=>dirname(resolver.resolve(name+'/package.json'));
|
|
58
|
-
const media=await createMediaStore({directory:join(profile.dir,'ekko-skins-media'),resolvePackage:id=>resolvePackage
|
|
66
|
+
const media=await createMediaStore({directory:join(profile.dir,'ekko-skins-media'),resolvePackage:id=>resolveSkinPackage(resolvePackage,id)});
|
|
59
67
|
child.effect(()=>()=>media.close(),'ekko-skins: private media store');
|
|
60
68
|
child.effect(()=>child.get('webServer').register({kind:'prefix',path:PREFIX,handler:createHandler({defaultSkin:config.defaultSkin,resolvePackage})}));
|
|
61
69
|
for(const [path,methods,fetch,requestBody]of [
|
package/src/video-settings.jsx
CHANGED
|
@@ -59,7 +59,7 @@ export function VideoSettings({skin}){
|
|
|
59
59
|
if(!data||!draft)return <section className="ekko-skin-options ekko-video-settings" data-testid="ekko-video-settings"><h3>启动视频</h3><p role="status">{message||'正在读取视频设置…'}</p><button type="button" className="ekko-skin-button" onClick={load}>重试</button></section>;
|
|
60
60
|
const update=patch=>setDraft({...draft,...patch}),fitKey=device==='mobile'?'mobileFit':'fit';
|
|
61
61
|
return <section className="ekko-skin-options ekko-video-settings" data-testid="ekko-video-settings" data-device={device}>
|
|
62
|
-
<div className="ekko-skin-toolbar"><h3>{skin==='miku'?'初音未来':'
|
|
62
|
+
<div className="ekko-skin-toolbar"><h3>{skin==='miku'?'初音未来':'玫瑰女王'} · 启动视频</h3><span className="ekko-skin-version">每款主题分别保存</span></div>
|
|
63
63
|
<div className="ekko-video-controls">
|
|
64
64
|
<label className="ekko-skin-option"><span>启用启动视频<small>页面启动时播放,可随时跳过</small></span><input type="checkbox" checked={draft.enabled} disabled={busy} onChange={e=>update({enabled:e.target.checked})}/></label>
|
|
65
65
|
<label className="ekko-skin-option"><span>播放声音<small>浏览器限制自动播放时先静音,点击可开启声音</small></span><input type="checkbox" checked={draft.sound} disabled={busy} onChange={e=>update({sound:e.target.checked})}/></label>
|