beauticode-dsh 1.0.7 → 1.0.12
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.zh-CN.md +2 -2
- package/agent.mjs +69 -52
- package/client.js +102 -22
- package/console.js +166 -46
- package/control-client.mjs +6 -4
- package/index.mjs +22 -8
- package/package.json +1 -1
- package/themes/internal-beyond/bg-canvas.png +0 -0
- package/ui-host.mjs +394 -31
- package/vendor/adapter-dsh/bridge.js +2 -4
- package/vendor/adapter-dsh/session.js +61 -27
- package/vendor/adapter-dsh/tray-handoff.js +13 -11
- package/vendor/core/apply-transaction.js +87 -45
- package/vendor/core/background-store.js +211 -351
- package/vendor/core/error-message.js +0 -8
- package/vendor/core/file-lock.js +13 -34
- package/vendor/core/index.js +1 -2
- package/vendor/core/media-server.js +22 -40
- package/vendor/core/media-source.js +36 -0
- package/vendor/core/media-validation.js +26 -19
- package/vendor/core/paths.js +0 -1
- package/vendor/core/process-liveness.js +0 -205
- package/vendor/core/skin-pack.js +0 -64
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
该 Cordis 插件向 DSH Web 注入 beautiCode 浏览器客户端,并提供本机鉴权接口。支持图片、MP4、播放位置、静音、摸鱼模式以及清除背景。
|
|
4
4
|
|
|
5
|
-
装好插件并运行 `dsh web`
|
|
5
|
+
装好插件并运行 `dsh web` 后,侧栏「设置」上方会出现「背景」:在 Windows 上点击图片或视频会打开系统文件选择器,选好后在页面内命名,背景立即应用并进入已保存主题;也可以清除、开关声音、切换或删除主题,以及打开「皮肤中心」安装已审核的社区皮肤。本地导入只在 Node 端保存绝对路径,并通过带 Range 支持的回环媒体服务读取原文件,不复制用户选择的图片或整段视频主文件。Windows 强制使用本地引用;原生选择器不可用时会明确报错,不会静默上传或复制媒体。非 Windows 环境保留兼容上传,并明确提示该模式会保存托管副本。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。设置 `BEAUTICODE_SKIN_CENTER` 或填写 `skin-center.json` 后,皮肤中心才会显示远程目录。
|
|
6
6
|
|
|
7
7
|
插件也会注册对话工具(`beauticode_*`)和斜杠命令(`/bg`、`/bg-theme`、`/bg-clear`)。不需要托盘;托盘若已在跑则复用它。
|
|
8
8
|
|
|
@@ -40,7 +40,7 @@ npx @deepseek-ai/dsh web
|
|
|
40
40
|
|
|
41
41
|
手动接入时:
|
|
42
42
|
|
|
43
|
-
1. 修改 `cordis.patch.
|
|
43
|
+
1. 修改 `cordis.patch.yml` 中的插件接入项;优先使用上面的安装命令自动完成接线。
|
|
44
44
|
2. 确保 DSH 和 beautiCode 使用相同的 `BEAUTICODE_DATA_ROOT`。
|
|
45
45
|
3. 自己运行 `dsh web`(或带 `--patch`)。
|
|
46
46
|
4. 打开页面后用侧栏「背景」,或输入 `/bg <本机绝对路径>`,或直接跟 AI 说把某个视频/图片设为背景。
|
package/agent.mjs
CHANGED
|
@@ -61,15 +61,20 @@ async function chineseError(error) {
|
|
|
61
61
|
return raw || "操作失败。";
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
async function unwrapApplyResult(result, fallbackMode, message) {
|
|
65
|
-
if (!result || result.ok === false) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
async function unwrapApplyResult(result, fallbackMode, message) {
|
|
65
|
+
if (!result || result.ok === false) {
|
|
66
|
+
const failure = new Error(await chineseError(result?.error || "操作失败。"));
|
|
67
|
+
if (result?.sourceMode != null) failure.sourceMode = result.sourceMode;
|
|
68
|
+
if (result?.timings != null) failure.timings = result.timings;
|
|
69
|
+
throw failure;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
generation: result.generation ?? null,
|
|
74
|
+
mode: result.mode ?? fallbackMode,
|
|
75
|
+
sourceMode: result.sourceMode ?? null,
|
|
76
|
+
timings: result.timings ?? null,
|
|
77
|
+
message,
|
|
73
78
|
...(result.theme
|
|
74
79
|
? {
|
|
75
80
|
theme: {
|
|
@@ -98,7 +103,7 @@ function normalizeActionOptions(dataRootOrOptions) {
|
|
|
98
103
|
: {};
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
function unwrapApply(result, fallbackMode, message) {
|
|
106
|
+
function unwrapApply(result, fallbackMode, message) {
|
|
102
107
|
if (!result || result.ok === false) {
|
|
103
108
|
fail(result?.error || "操作失败。");
|
|
104
109
|
}
|
|
@@ -116,18 +121,24 @@ function unwrapApply(result, fallbackMode, message) {
|
|
|
116
121
|
},
|
|
117
122
|
}
|
|
118
123
|
: {}),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function presentStatus(status) {
|
|
123
|
-
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function presentStatus(status) {
|
|
128
|
+
const background = status.manifest?.background ?? status.background ?? null;
|
|
129
|
+
return {
|
|
124
130
|
ok: true,
|
|
125
131
|
hostReady: status.hostReady === true || status.sessions > 0,
|
|
126
132
|
sessions: status.sessions ?? 0,
|
|
127
133
|
fish: status.fish === true,
|
|
128
134
|
muted: status.muted !== false,
|
|
129
135
|
tone: status.tone ?? "dark",
|
|
130
|
-
background
|
|
136
|
+
background,
|
|
137
|
+
sourceMode: background
|
|
138
|
+
? background.source?.kind === "local"
|
|
139
|
+
? "local"
|
|
140
|
+
: "managed"
|
|
141
|
+
: "clear",
|
|
131
142
|
themeId: typeof status.themeId === "string" && status.themeId ? status.themeId : null,
|
|
132
143
|
message: formatStatusText(status),
|
|
133
144
|
};
|
|
@@ -167,21 +178,26 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
167
178
|
}
|
|
168
179
|
const effects = effectsForPreset(options?.effects?.preset) || options?.effects || null;
|
|
169
180
|
const persistTheme = options?.persistTheme !== false;
|
|
170
|
-
const themeName =
|
|
171
|
-
String(options?.themeName ?? "").trim() ||
|
|
172
|
-
themeNameFromFilePath(inspected.path, "图片");
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
const themeName =
|
|
182
|
+
String(options?.themeName ?? "").trim() ||
|
|
183
|
+
themeNameFromFilePath(inspected.path, "图片");
|
|
184
|
+
const source = options?.source === "managed" ? "managed" : "local";
|
|
185
|
+
const input = { type: "image", imagePath: inspected.path, source };
|
|
186
|
+
if (effects) input.effects = effects;
|
|
187
|
+
const resolved = await backend();
|
|
188
|
+
if (resolved.kind === "tray") {
|
|
189
|
+
const result = await request({
|
|
190
|
+
method: "POST",
|
|
191
|
+
path: persistTheme ? "/theme/apply" : "/apply/image",
|
|
192
|
+
body: persistTheme
|
|
193
|
+
? { name: themeName, input }
|
|
194
|
+
: {
|
|
195
|
+
imagePath: inspected.path,
|
|
196
|
+
source,
|
|
197
|
+
...(effects ? { effects } : {}),
|
|
198
|
+
},
|
|
199
|
+
signal,
|
|
200
|
+
});
|
|
185
201
|
return unwrapApplyResult(
|
|
186
202
|
result,
|
|
187
203
|
"image",
|
|
@@ -189,10 +205,10 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
189
205
|
? `已将「${result.theme?.name || themeName}」设为背景。`
|
|
190
206
|
: "已将图片设为背景。",
|
|
191
207
|
);
|
|
192
|
-
}
|
|
193
|
-
const applied = persistTheme
|
|
194
|
-
? await resolved.session.
|
|
195
|
-
: await resolved.session.apply(input);
|
|
208
|
+
}
|
|
209
|
+
const applied = persistTheme
|
|
210
|
+
? await resolved.session.applyAndSaveTheme(input, themeName)
|
|
211
|
+
: await resolved.session.apply(input);
|
|
196
212
|
return unwrapApplyResult(
|
|
197
213
|
applied,
|
|
198
214
|
"image",
|
|
@@ -245,11 +261,12 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
245
261
|
fail("beauticode_apply_video 只接受 .mp4 文件。");
|
|
246
262
|
}
|
|
247
263
|
const persistTheme = input?.persistTheme !== false;
|
|
248
|
-
const themeName =
|
|
249
|
-
String(input?.themeName ?? "").trim() ||
|
|
250
|
-
themeNameFromFilePath(inspected.path, "视频");
|
|
251
|
-
const
|
|
252
|
-
const
|
|
264
|
+
const themeName =
|
|
265
|
+
String(input?.themeName ?? "").trim() ||
|
|
266
|
+
themeNameFromFilePath(inspected.path, "视频");
|
|
267
|
+
const source = input?.source === "managed" ? "managed" : "local";
|
|
268
|
+
const body = { videoPath: inspected.path, persistTheme, themeName, source };
|
|
269
|
+
const localInput = { type: "video", videoPath: inspected.path, source };
|
|
253
270
|
if (typeof input.poster === "string" && input.poster.trim()) {
|
|
254
271
|
const poster = await inspectLocalMedia(input.poster);
|
|
255
272
|
if (!poster.ok) fail(poster.error);
|
|
@@ -265,14 +282,14 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
265
282
|
body.startAt = startAt;
|
|
266
283
|
localInput.startAt = startAt;
|
|
267
284
|
}
|
|
268
|
-
const resolved = await backend();
|
|
269
|
-
if (resolved.kind === "tray") {
|
|
270
|
-
const result = await request({
|
|
271
|
-
method: "POST",
|
|
272
|
-
path: "/apply/video",
|
|
273
|
-
body,
|
|
274
|
-
signal,
|
|
275
|
-
});
|
|
285
|
+
const resolved = await backend();
|
|
286
|
+
if (resolved.kind === "tray") {
|
|
287
|
+
const result = await request({
|
|
288
|
+
method: "POST",
|
|
289
|
+
path: persistTheme ? "/theme/apply" : "/apply/video",
|
|
290
|
+
body: persistTheme ? { name: themeName, input: localInput } : body,
|
|
291
|
+
signal,
|
|
292
|
+
});
|
|
276
293
|
return unwrapApplyResult(
|
|
277
294
|
result,
|
|
278
295
|
"video",
|
|
@@ -280,10 +297,10 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
280
297
|
? `已将「${result.theme?.name || themeName}」设为背景。`
|
|
281
298
|
: "已将视频设为背景。",
|
|
282
299
|
);
|
|
283
|
-
}
|
|
284
|
-
const applied = persistTheme
|
|
285
|
-
? await resolved.session.
|
|
286
|
-
: await resolved.session.apply(localInput);
|
|
300
|
+
}
|
|
301
|
+
const applied = persistTheme
|
|
302
|
+
? await resolved.session.applyAndSaveTheme(localInput, themeName)
|
|
303
|
+
: await resolved.session.apply(localInput);
|
|
287
304
|
return unwrapApplyResult(
|
|
288
305
|
applied,
|
|
289
306
|
"video",
|
package/client.js
CHANGED
|
@@ -197,25 +197,103 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
|
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
200
|
+
function describeVideoState(video, phase) {
|
|
201
|
+
const mediaErrorNames = {
|
|
202
|
+
1: "MEDIA_ERR_ABORTED",
|
|
203
|
+
2: "MEDIA_ERR_NETWORK",
|
|
204
|
+
3: "MEDIA_ERR_DECODE",
|
|
205
|
+
4: "MEDIA_ERR_SRC_NOT_SUPPORTED",
|
|
206
|
+
};
|
|
207
|
+
const code = Number(video?.error?.code) || 0;
|
|
208
|
+
const mediaError = code ? mediaErrorNames[code] || `MEDIA_ERR_${code}` : "none";
|
|
209
|
+
return `${phase};mediaError=${mediaError};readyState=${video?.readyState ?? -1};networkState=${video?.networkState ?? -1};paused=${Boolean(video?.paused)}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function waitForVideo(video, timeoutMs = 20_000) {
|
|
213
|
+
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) return Promise.resolve();
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const done = () => {
|
|
216
|
+
cleanup();
|
|
217
|
+
resolve();
|
|
218
|
+
};
|
|
219
|
+
const failed = () => {
|
|
220
|
+
cleanup();
|
|
221
|
+
reject(new Error(describeVideoState(video, "MP4 加载或解码失败")));
|
|
222
|
+
};
|
|
223
|
+
const timer = setTimeout(() => {
|
|
224
|
+
cleanup();
|
|
225
|
+
reject(new Error(describeVideoState(video, "等待视频首帧超时")));
|
|
226
|
+
}, timeoutMs);
|
|
227
|
+
const cleanup = () => {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
video.removeEventListener("loadeddata", done);
|
|
230
|
+
video.removeEventListener("error", failed);
|
|
231
|
+
};
|
|
215
232
|
video.addEventListener("loadeddata", done, { once: true });
|
|
216
233
|
video.addEventListener("error", failed, { once: true });
|
|
217
|
-
});
|
|
218
|
-
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function waitForStablePlayback(video, timeoutMs = 8_000) {
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const startedAt = performance.now();
|
|
240
|
+
const initialTime = Number.isFinite(video.currentTime) ? video.currentTime : 0;
|
|
241
|
+
let frameSeen = false;
|
|
242
|
+
let settled = false;
|
|
243
|
+
let frameRequest = null;
|
|
244
|
+
|
|
245
|
+
const cleanup = () => {
|
|
246
|
+
clearInterval(timer);
|
|
247
|
+
video.removeEventListener("playing", check);
|
|
248
|
+
video.removeEventListener("timeupdate", check);
|
|
249
|
+
video.removeEventListener("error", failed);
|
|
250
|
+
if (frameRequest != null && typeof video.cancelVideoFrameCallback === "function") {
|
|
251
|
+
video.cancelVideoFrameCallback(frameRequest);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const finish = (error) => {
|
|
255
|
+
if (settled) return;
|
|
256
|
+
settled = true;
|
|
257
|
+
cleanup();
|
|
258
|
+
if (error) reject(error);
|
|
259
|
+
else resolve();
|
|
260
|
+
};
|
|
261
|
+
const failed = () => {
|
|
262
|
+
finish(new Error(describeVideoState(video, "视频解码器报告失败")));
|
|
263
|
+
};
|
|
264
|
+
const check = () => {
|
|
265
|
+
if (settled) return;
|
|
266
|
+
if (video.error) {
|
|
267
|
+
failed();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const progressed =
|
|
271
|
+
Number.isFinite(video.currentTime) && video.currentTime >= initialTime + 0.08;
|
|
272
|
+
if (
|
|
273
|
+
video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA &&
|
|
274
|
+
!video.paused &&
|
|
275
|
+
(frameSeen || progressed)
|
|
276
|
+
) {
|
|
277
|
+
finish();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (performance.now() - startedAt >= timeoutMs) {
|
|
281
|
+
finish(new Error(describeVideoState(video, "视频未在稳定窗口内输出首帧")));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const timer = setInterval(check, 100);
|
|
285
|
+
video.addEventListener("playing", check);
|
|
286
|
+
video.addEventListener("timeupdate", check);
|
|
287
|
+
video.addEventListener("error", failed, { once: true });
|
|
288
|
+
if (typeof video.requestVideoFrameCallback === "function") {
|
|
289
|
+
frameRequest = video.requestVideoFrameCallback(() => {
|
|
290
|
+
frameSeen = true;
|
|
291
|
+
check();
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
check();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
219
297
|
|
|
220
298
|
function seekVideo(video, value) {
|
|
221
299
|
const requested = Number(value);
|
|
@@ -299,11 +377,13 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
|
|
|
299
377
|
video.poster = payload.imageUrl;
|
|
300
378
|
video.src = payload.videoUrl;
|
|
301
379
|
node.replaceChildren(image, video);
|
|
302
|
-
await waitForVideo(video);
|
|
303
|
-
if (activePayload !== payload) return;
|
|
304
|
-
seekVideo(video, payload.startAt);
|
|
305
|
-
await playWithPreference(video);
|
|
306
|
-
|
|
380
|
+
await waitForVideo(video);
|
|
381
|
+
if (activePayload !== payload) return;
|
|
382
|
+
seekVideo(video, payload.startAt);
|
|
383
|
+
await playWithPreference(video);
|
|
384
|
+
await waitForStablePlayback(video);
|
|
385
|
+
if (activePayload !== payload) return;
|
|
386
|
+
document.documentElement.dataset.bcActive = "true";
|
|
307
387
|
document.documentElement.dataset.bcMedia = "video";
|
|
308
388
|
const visible = video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && !video.paused;
|
|
309
389
|
await acknowledgeRender(payload, visible, visible, visible ? null : "视频未开始播放");
|
package/console.js
CHANGED
|
@@ -34,14 +34,27 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-toggle:hover{ba
|
|
|
34
34
|
body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-list{border-color:rgba(0,0,0,.08);background:#f4f6f8}
|
|
35
35
|
#beauticode-console-pop .bc-theme-row{display:flex;gap:4px;align-items:center}
|
|
36
36
|
#beauticode-console-pop .bc-theme-item{cursor:pointer;display:block;flex:1;min-width:0;height:32px;padding:0 8px;border:none;border-radius:8px;background:transparent;color:inherit;font-size:13px;line-height:32px;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
37
|
-
#beauticode-console-pop .bc-theme-item:hover{background:rgba(255,255,255,.08)}
|
|
38
|
-
body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{background:rgba(0,0,0,.06)}
|
|
37
|
+
#beauticode-console-pop .bc-theme-item:hover{background:rgba(255,255,255,.08)}
|
|
38
|
+
body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{background:rgba(0,0,0,.06)}
|
|
39
|
+
#beauticode-console-pop .bc-source{margin-left:6px;font-size:11px;opacity:.62}
|
|
39
40
|
#beauticode-console-pop .bc-theme-del{cursor:pointer;flex:none;width:32px;height:32px;padding:0;border:none;border-radius:8px;background:transparent;color:inherit;font-size:12px;opacity:.72}
|
|
40
41
|
#beauticode-console-pop .bc-theme-del:hover{background:rgba(255,255,255,.08);opacity:1}
|
|
41
42
|
body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{background:rgba(0,0,0,.06)}
|
|
42
|
-
#beauticode-console-pop .bc-msg{color:var(--dsw-alias-label-secondary,#9aa3ad);font-size:12px;line-height:16px;margin:6px 0 0;max-height:3.2em;overflow:hidden}
|
|
43
|
-
#beauticode-console-file{display:none !important}
|
|
44
|
-
|
|
43
|
+
#beauticode-console-pop .bc-msg{color:var(--dsw-alias-label-secondary,#9aa3ad);font-size:12px;line-height:16px;margin:6px 0 0;max-height:3.2em;overflow:hidden}
|
|
44
|
+
#beauticode-console-file{display:none !important}
|
|
45
|
+
#beauticode-name-dialog{position:fixed;inset:0;z-index:3000;display:grid;place-items:center;padding:24px;background:rgba(0,0,0,.42);font:inherit}
|
|
46
|
+
#beauticode-name-dialog .bc-name-card{display:flex;flex-direction:column;gap:10px;width:min(380px,calc(100vw - 48px));padding:18px;border:1px solid rgba(255,255,255,.12);border-radius:18px;background:#2c323c;color:#e8eaed;box-shadow:0 18px 60px rgba(0,0,0,.35)}
|
|
47
|
+
#beauticode-name-dialog .bc-name-title{margin:0;font-size:15px;font-weight:600}
|
|
48
|
+
#beauticode-name-dialog .bc-name-file,#beauticode-name-dialog .bc-name-note,#beauticode-name-dialog .bc-name-error{margin:0;color:#aeb7c3;font-size:12px;line-height:18px;overflow-wrap:anywhere}
|
|
49
|
+
#beauticode-name-dialog .bc-name-error{color:#ff9a9a}
|
|
50
|
+
#beauticode-name-dialog input{height:36px;padding:0 10px;border:1px solid rgba(255,255,255,.14);border-radius:10px;background:rgba(255,255,255,.08);color:inherit;font:inherit;font-size:13px}
|
|
51
|
+
#beauticode-name-dialog .bc-name-actions{display:flex;justify-content:flex-end;gap:8px}
|
|
52
|
+
#beauticode-name-dialog button{cursor:pointer;height:34px;padding:0 13px;border:1px solid rgba(255,255,255,.12);border-radius:10px;background:rgba(255,255,255,.08);color:inherit;font:inherit}
|
|
53
|
+
#beauticode-name-dialog button[data-name="confirm"]{background:#4f7cff;border-color:#4f7cff;color:#fff}
|
|
54
|
+
body:not([data-ds-dark-theme]) #beauticode-name-dialog .bc-name-card{background:#fff;color:#1b1f24;border-color:rgba(0,0,0,.1)}
|
|
55
|
+
body:not([data-ds-dark-theme]) #beauticode-name-dialog input,body:not([data-ds-dark-theme]) #beauticode-name-dialog button{border-color:rgba(0,0,0,.12);background:#f4f6f8}
|
|
56
|
+
body:not([data-ds-dark-theme]) #beauticode-name-dialog button[data-name="confirm"]{background:#315efb;border-color:#315efb;color:#fff}
|
|
57
|
+
`;
|
|
45
58
|
document.head.append(style);
|
|
46
59
|
|
|
47
60
|
const host = document.createElement("div");
|
|
@@ -144,7 +157,7 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{backg
|
|
|
144
157
|
msgEl.textContent = text;
|
|
145
158
|
}
|
|
146
159
|
|
|
147
|
-
function renderStatus(data) {
|
|
160
|
+
function renderStatus(data) {
|
|
148
161
|
if (!data?.ok) {
|
|
149
162
|
statusEl.textContent = data?.error || "未就绪";
|
|
150
163
|
return;
|
|
@@ -157,7 +170,13 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{backg
|
|
|
157
170
|
: data.media === "image"
|
|
158
171
|
? "图片"
|
|
159
172
|
: "无背景";
|
|
160
|
-
|
|
173
|
+
const sourceLabel =
|
|
174
|
+
data.sourceMode === "local"
|
|
175
|
+
? "本地引用"
|
|
176
|
+
: data.sourceMode === "managed"
|
|
177
|
+
? "托管副本"
|
|
178
|
+
: "";
|
|
179
|
+
statusEl.textContent = sourceLabel ? `${label} · ${sourceLabel}` : label;
|
|
161
180
|
if (typeof data.themeId === "string" && data.themeId) {
|
|
162
181
|
currentThemeId = data.themeId;
|
|
163
182
|
} else if (data.atmosphere === "gallery") {
|
|
@@ -181,7 +200,9 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{backg
|
|
|
181
200
|
theme.bundled === true
|
|
182
201
|
? ""
|
|
183
202
|
: `<button type="button" class="bc-theme-del" data-theme-delete="${escapeAttr(theme.id)}" data-theme-name="${escapeAttr(theme.name)}" aria-label="删除 ${escapeAttr(theme.name)}">删</button>`;
|
|
184
|
-
|
|
203
|
+
const source =
|
|
204
|
+
theme.sourceMode === "local" ? "本地" : theme.bundled ? "内置" : "托管";
|
|
205
|
+
return `<div class="bc-theme-row"><button type="button" class="bc-theme-item" data-theme-id="${escapeAttr(theme.id)}">${escapeText(theme.name)}<span class="bc-source">${source}</span></button>${del}</div>`;
|
|
185
206
|
})
|
|
186
207
|
.join("");
|
|
187
208
|
const selected = themes.find((theme) => theme.id === currentThemeId);
|
|
@@ -206,9 +227,12 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{backg
|
|
|
206
227
|
...(init?.headers || {}),
|
|
207
228
|
},
|
|
208
229
|
});
|
|
209
|
-
const body = await response.json().catch(() => null);
|
|
210
|
-
if (!response.ok || body?.ok === false) {
|
|
211
|
-
|
|
230
|
+
const body = await response.json().catch(() => null);
|
|
231
|
+
if (!response.ok || body?.ok === false) {
|
|
232
|
+
const error = new Error(body?.error || `请求失败(${response.status})`);
|
|
233
|
+
error.status = response.status;
|
|
234
|
+
error.code = body?.code || "";
|
|
235
|
+
throw error;
|
|
212
236
|
}
|
|
213
237
|
return body;
|
|
214
238
|
}
|
|
@@ -222,44 +246,138 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{backg
|
|
|
222
246
|
}
|
|
223
247
|
|
|
224
248
|
async function run(task) {
|
|
225
|
-
if (busy) return;
|
|
226
|
-
busy = true;
|
|
249
|
+
if (busy) return;
|
|
250
|
+
busy = true;
|
|
251
|
+
let afterRun = null;
|
|
227
252
|
for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item, .bc-theme-del")) button.disabled = true;
|
|
228
253
|
showMessage("");
|
|
229
254
|
try {
|
|
230
|
-
const result = await task();
|
|
231
|
-
if (result?.
|
|
232
|
-
if (result?.
|
|
255
|
+
const result = await task();
|
|
256
|
+
if (typeof result?.afterRun === "function") afterRun = result.afterRun;
|
|
257
|
+
if (result?.theme?.id) currentThemeId = result.theme.id;
|
|
258
|
+
if (result?.message) {
|
|
259
|
+
const source =
|
|
260
|
+
result.sourceMode === "local"
|
|
261
|
+
? "本地引用,未复制主媒体"
|
|
262
|
+
: result.sourceMode === "managed"
|
|
263
|
+
? "托管副本"
|
|
264
|
+
: "";
|
|
265
|
+
const totalMs = Number(result.importTimings?.applyAndSaveMs ?? result.timings?.totalMs);
|
|
266
|
+
const duration = Number.isFinite(totalMs) ? `${Math.round(totalMs)} ms` : "";
|
|
267
|
+
showMessage([result.message, source, duration].filter(Boolean).join(" · "));
|
|
268
|
+
}
|
|
233
269
|
await refresh();
|
|
234
270
|
} catch (error) {
|
|
235
271
|
showMessage(error instanceof Error ? error.message : String(error));
|
|
236
272
|
} finally {
|
|
237
|
-
busy = false;
|
|
238
|
-
for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item, .bc-theme-del")) button.disabled = false;
|
|
273
|
+
busy = false;
|
|
274
|
+
for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item, .bc-theme-del")) button.disabled = false;
|
|
239
275
|
}
|
|
276
|
+
if (afterRun) queueMicrotask(afterRun);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function validateThemeName(value) {
|
|
280
|
+
const name = String(value || "").trim();
|
|
281
|
+
if (!name) return "主题名不能为空。";
|
|
282
|
+
if (name.length > 80) return "主题名不能超过 80 个字符。";
|
|
283
|
+
if (/[<>:"/\\|?*]/.test(name) || /[\u0000-\u001f]/.test(name)) {
|
|
284
|
+
return '主题名不能包含 < > : " / \\ | ? * 或控制字符。';
|
|
285
|
+
}
|
|
286
|
+
return "";
|
|
240
287
|
}
|
|
241
288
|
|
|
242
|
-
function
|
|
289
|
+
function defaultThemeName(fileName) {
|
|
243
290
|
const suggested = String(fileName || "")
|
|
244
291
|
.replace(/\.[^.]+$/, "")
|
|
245
292
|
.trim()
|
|
246
293
|
.slice(0, 80);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
294
|
+
return suggested || "新主题";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function askThemeName(fileName, suggestedName, options = {}) {
|
|
298
|
+
return new Promise((resolve) => {
|
|
299
|
+
const dialog = document.createElement("div");
|
|
300
|
+
dialog.id = "beauticode-name-dialog";
|
|
301
|
+
dialog.innerHTML =
|
|
302
|
+
'<div class="bc-name-card" role="dialog" aria-modal="true" aria-labelledby="beauticode-name-title">' +
|
|
303
|
+
'<p id="beauticode-name-title" class="bc-name-title">给主题取个名字</p>' +
|
|
304
|
+
`<p class="bc-name-file">${escapeText(fileName)}</p>` +
|
|
305
|
+
(options.compatibilityUpload
|
|
306
|
+
? '<p class="bc-name-note">原生选择器不可用,兼容模式会复制媒体文件。</p>'
|
|
307
|
+
: "") +
|
|
308
|
+
`<input type="text" maxlength="80" aria-label="主题名称" value="${escapeAttr(suggestedName || defaultThemeName(fileName))}"/>` +
|
|
309
|
+
'<p class="bc-name-error" hidden></p>' +
|
|
310
|
+
'<div class="bc-name-actions"><button type="button" data-name="cancel">取消</button><button type="button" data-name="confirm">保存并应用</button></div>' +
|
|
311
|
+
"</div>";
|
|
312
|
+
document.body.append(dialog);
|
|
313
|
+
const input = dialog.querySelector('input[aria-label="主题名称"]');
|
|
314
|
+
const errorEl = dialog.querySelector(".bc-name-error");
|
|
315
|
+
let closed = false;
|
|
316
|
+
|
|
317
|
+
const close = (value) => {
|
|
318
|
+
if (closed) return;
|
|
319
|
+
closed = true;
|
|
320
|
+
dialog.remove();
|
|
321
|
+
resolve(value);
|
|
322
|
+
};
|
|
323
|
+
const confirm = () => {
|
|
324
|
+
const error = validateThemeName(input.value);
|
|
325
|
+
if (error) {
|
|
326
|
+
errorEl.hidden = false;
|
|
327
|
+
errorEl.textContent = error;
|
|
328
|
+
input.focus();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
close(input.value.trim());
|
|
332
|
+
};
|
|
333
|
+
dialog.querySelector('[data-name="confirm"]').addEventListener("click", confirm);
|
|
334
|
+
dialog.querySelector('[data-name="cancel"]').addEventListener("click", () => close(null));
|
|
335
|
+
input.addEventListener("input", () => {
|
|
336
|
+
errorEl.hidden = true;
|
|
337
|
+
errorEl.textContent = "";
|
|
338
|
+
});
|
|
339
|
+
input.addEventListener("keydown", (event) => {
|
|
340
|
+
if (event.key === "Enter") confirm();
|
|
341
|
+
if (event.key === "Escape") close(null);
|
|
342
|
+
});
|
|
343
|
+
dialog.addEventListener("click", (event) => {
|
|
344
|
+
if (event.target === dialog) close(null);
|
|
345
|
+
});
|
|
346
|
+
input.focus();
|
|
347
|
+
input.select();
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function pickAndImport(kind) {
|
|
352
|
+
let picked;
|
|
353
|
+
try {
|
|
354
|
+
picked = await request("/__beauticode/ui/pick", {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: { "content-type": "application/json" },
|
|
357
|
+
body: JSON.stringify({ kind }),
|
|
358
|
+
});
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error?.code !== "native_picker_unavailable") throw error;
|
|
361
|
+
return {
|
|
362
|
+
ok: true,
|
|
363
|
+
afterRun: () => {
|
|
364
|
+
fileInput.accept = kind === "video" ? VIDEO_ACCEPT : IMAGE_ACCEPT;
|
|
365
|
+
fileInput.dataset.compatibilityUpload = "true";
|
|
366
|
+
fileInput.click();
|
|
367
|
+
},
|
|
368
|
+
};
|
|
261
369
|
}
|
|
262
|
-
return
|
|
370
|
+
if (picked.cancelled) return { ok: true };
|
|
371
|
+
const themeName = await askThemeName(
|
|
372
|
+
picked.name,
|
|
373
|
+
picked.suggestedThemeName,
|
|
374
|
+
);
|
|
375
|
+
if (!themeName) return { ok: true };
|
|
376
|
+
return request("/__beauticode/ui/import-selected", {
|
|
377
|
+
method: "POST",
|
|
378
|
+
headers: { "content-type": "application/json" },
|
|
379
|
+
body: JSON.stringify({ selectionId: picked.selectionId, themeName }),
|
|
380
|
+
});
|
|
263
381
|
}
|
|
264
382
|
|
|
265
383
|
trigger.addEventListener("click", (event) => {
|
|
@@ -267,12 +385,10 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{backg
|
|
|
267
385
|
setOpen(pop.hidden);
|
|
268
386
|
});
|
|
269
387
|
pop.querySelector('[data-act="image"]').addEventListener("click", () => {
|
|
270
|
-
|
|
271
|
-
fileInput.click();
|
|
388
|
+
void run(() => pickAndImport("image"));
|
|
272
389
|
});
|
|
273
390
|
pop.querySelector('[data-act="video"]').addEventListener("click", () => {
|
|
274
|
-
|
|
275
|
-
fileInput.click();
|
|
391
|
+
void run(() => pickAndImport("video"));
|
|
276
392
|
});
|
|
277
393
|
pop.querySelector('[data-act="gallery"]').addEventListener("click", (event) => {
|
|
278
394
|
event.stopPropagation();
|
|
@@ -338,21 +454,25 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{backg
|
|
|
338
454
|
});
|
|
339
455
|
fileInput.addEventListener("change", () => {
|
|
340
456
|
const file = fileInput.files?.[0];
|
|
457
|
+
const compatibilityUpload = fileInput.dataset.compatibilityUpload === "true";
|
|
341
458
|
fileInput.value = "";
|
|
459
|
+
delete fileInput.dataset.compatibilityUpload;
|
|
342
460
|
if (!file) return;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
461
|
+
void run(async () => {
|
|
462
|
+
const themeName = await askThemeName(file.name, defaultThemeName(file.name), {
|
|
463
|
+
compatibilityUpload,
|
|
464
|
+
});
|
|
465
|
+
if (!themeName) return { ok: true };
|
|
466
|
+
return request("/__beauticode/ui/import", {
|
|
347
467
|
method: "POST",
|
|
348
468
|
headers: {
|
|
349
469
|
"x-beauticode-filename": encodeURIComponent(file.name),
|
|
350
|
-
"x-beauticode-theme-name": encodeURIComponent(themeName),
|
|
470
|
+
"x-beauticode-theme-name": encodeURIComponent(themeName),
|
|
351
471
|
},
|
|
352
472
|
body: file,
|
|
353
|
-
})
|
|
354
|
-
);
|
|
355
|
-
});
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
});
|
|
356
476
|
|
|
357
477
|
document.addEventListener("click", (event) => {
|
|
358
478
|
if (pop.hidden) return;
|
package/control-client.mjs
CHANGED
|
@@ -505,10 +505,12 @@ export async function callDshControl(dataRoot, spec) {
|
|
|
505
505
|
typeof payload?.error === "string"
|
|
506
506
|
? payload.error
|
|
507
507
|
: `托盘请求失败(HTTP ${response.status})。`;
|
|
508
|
-
const error = new Error(message);
|
|
509
|
-
error.statusCode = response.status;
|
|
510
|
-
error.payload = payload;
|
|
511
|
-
|
|
508
|
+
const error = new Error(message);
|
|
509
|
+
error.statusCode = response.status;
|
|
510
|
+
error.payload = payload;
|
|
511
|
+
if (payload?.sourceMode != null) error.sourceMode = payload.sourceMode;
|
|
512
|
+
if (payload?.timings != null) error.timings = payload.timings;
|
|
513
|
+
throw error;
|
|
512
514
|
}
|
|
513
515
|
return payload;
|
|
514
516
|
}
|