beauticode-dsh 1.0.4 → 1.0.7
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 +1 -1
- package/agent.mjs +138 -23
- package/cli.js +58 -23
- package/console.js +78 -22
- package/index.mjs +5 -0
- package/package.json +1 -1
- package/ui-host.mjs +65 -9
- package/vendor/adapter-dsh/session.js +12 -10
- package/vendor/core/background-store.js +104 -8
- package/vendor/core/error-message.js +2 -0
- package/vendor/core/paths.js +1 -0
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
该 Cordis 插件向 DSH Web 注入 beautiCode 浏览器客户端,并提供本机鉴权接口。支持图片、MP4、播放位置、静音、摸鱼模式以及清除背景。
|
|
4
4
|
|
|
5
|
-
装好插件并运行 `dsh web` 后,侧栏「设置」上方会出现「背景」:可从文件夹选图片或 MP4
|
|
5
|
+
装好插件并运行 `dsh web` 后,侧栏「设置」上方会出现「背景」:可从文件夹选图片或 MP4、清除、开关声音、切换或删除已保存主题,以及打开「皮肤中心」安装已审核的社区皮肤。导入的图片和视频会自动进入已保存主题。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。设置 `BEAUTICODE_SKIN_CENTER` 或填写 `skin-center.json` 后,皮肤中心才会显示远程目录。
|
|
6
6
|
|
|
7
7
|
插件也会注册对话工具(`beauticode_*`)和斜杠命令(`/bg`、`/bg-theme`、`/bg-clear`)。不需要托盘;托盘若已在跑则复用它。
|
|
8
8
|
|
package/agent.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import {
|
|
2
3
|
callDshControl,
|
|
3
4
|
formatStatusText,
|
|
@@ -5,7 +6,7 @@ import {
|
|
|
5
6
|
matchSavedTheme,
|
|
6
7
|
stripPathQuotes,
|
|
7
8
|
} from "./control-client.mjs";
|
|
8
|
-
import { resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs";
|
|
9
|
+
import { loadAdapter, resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs";
|
|
9
10
|
import { ATMOSPHERE_PRESETS, effectsForPreset, presetImagePath } from "./presets.mjs";
|
|
10
11
|
|
|
11
12
|
const APPLY_TIMEOUT_MS = 180_000;
|
|
@@ -32,6 +33,55 @@ function fail(error) {
|
|
|
32
33
|
throw error instanceof Error ? error : new Error(String(error));
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
export function themeNameFromFilePath(filePath, fallback = "主题") {
|
|
37
|
+
const base = path.basename(String(filePath ?? "").replaceAll("\\", "/"));
|
|
38
|
+
const ext = path.extname(base);
|
|
39
|
+
let name = (ext ? base.slice(0, -ext.length) : base).trim();
|
|
40
|
+
name = name
|
|
41
|
+
.replace(/[<>:"/\\|?*]/g, " ")
|
|
42
|
+
.replace(/[\u0000-\u001f]/g, "")
|
|
43
|
+
.replace(/\s+/g, " ")
|
|
44
|
+
.trim();
|
|
45
|
+
if (!name) name = fallback;
|
|
46
|
+
if (name.length > 80) name = name.slice(0, 80).trim();
|
|
47
|
+
if (!name) name = fallback;
|
|
48
|
+
return name;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function chineseError(error) {
|
|
52
|
+
const raw = error instanceof Error ? error.message : String(error ?? "");
|
|
53
|
+
try {
|
|
54
|
+
const adapter = await loadAdapter();
|
|
55
|
+
if (typeof adapter.toChineseErrorMessage === "function") {
|
|
56
|
+
return adapter.toChineseErrorMessage(error);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
/* keep raw */
|
|
60
|
+
}
|
|
61
|
+
return raw || "操作失败。";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function unwrapApplyResult(result, fallbackMode, message) {
|
|
65
|
+
if (!result || result.ok === false) {
|
|
66
|
+
fail(await chineseError(result?.error || "操作失败。"));
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
generation: result.generation ?? null,
|
|
71
|
+
mode: result.mode ?? fallbackMode,
|
|
72
|
+
message,
|
|
73
|
+
...(result.theme
|
|
74
|
+
? {
|
|
75
|
+
theme: {
|
|
76
|
+
id: result.theme.id,
|
|
77
|
+
name: result.theme.name,
|
|
78
|
+
type: result.theme.type ?? null,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
: {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
35
85
|
function commandResultFromError(error) {
|
|
36
86
|
return {
|
|
37
87
|
kind: "error",
|
|
@@ -57,6 +107,15 @@ function unwrapApply(result, fallbackMode, message) {
|
|
|
57
107
|
generation: result.generation ?? null,
|
|
58
108
|
mode: result.mode ?? fallbackMode,
|
|
59
109
|
message,
|
|
110
|
+
...(result.theme
|
|
111
|
+
? {
|
|
112
|
+
theme: {
|
|
113
|
+
id: result.theme.id,
|
|
114
|
+
name: result.theme.name,
|
|
115
|
+
type: result.theme.type ?? null,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
: {}),
|
|
60
119
|
};
|
|
61
120
|
}
|
|
62
121
|
|
|
@@ -69,6 +128,7 @@ function presentStatus(status) {
|
|
|
69
128
|
muted: status.muted !== false,
|
|
70
129
|
tone: status.tone ?? "dark",
|
|
71
130
|
background: status.manifest?.background ?? status.background ?? null,
|
|
131
|
+
themeId: typeof status.themeId === "string" && status.themeId ? status.themeId : null,
|
|
72
132
|
message: formatStatusText(status),
|
|
73
133
|
};
|
|
74
134
|
}
|
|
@@ -106,24 +166,40 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
106
166
|
fail("beauticode_apply_image 只接受图片文件。");
|
|
107
167
|
}
|
|
108
168
|
const effects = effectsForPreset(options?.effects?.preset) || options?.effects || null;
|
|
169
|
+
const persistTheme = options?.persistTheme !== false;
|
|
170
|
+
const themeName =
|
|
171
|
+
String(options?.themeName ?? "").trim() ||
|
|
172
|
+
themeNameFromFilePath(inspected.path, "图片");
|
|
109
173
|
const input = { type: "image", imagePath: inspected.path };
|
|
110
174
|
if (effects) input.effects = effects;
|
|
111
175
|
const resolved = await backend();
|
|
112
176
|
if (resolved.kind === "tray") {
|
|
113
|
-
const body = { imagePath: inspected.path };
|
|
177
|
+
const body = { imagePath: inspected.path, persistTheme, themeName };
|
|
114
178
|
if (effects) body.effects = effects;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
179
|
+
const result = await request({
|
|
180
|
+
method: "POST",
|
|
181
|
+
path: "/apply/image",
|
|
182
|
+
body,
|
|
183
|
+
signal,
|
|
184
|
+
});
|
|
185
|
+
return unwrapApplyResult(
|
|
186
|
+
result,
|
|
122
187
|
"image",
|
|
123
|
-
|
|
188
|
+
persistTheme
|
|
189
|
+
? `已将「${result.theme?.name || themeName}」设为背景。`
|
|
190
|
+
: "已将图片设为背景。",
|
|
124
191
|
);
|
|
125
192
|
}
|
|
126
|
-
|
|
193
|
+
const applied = persistTheme
|
|
194
|
+
? await resolved.session.applyImportedMedia(input, { name: themeName })
|
|
195
|
+
: await resolved.session.apply(input);
|
|
196
|
+
return unwrapApplyResult(
|
|
197
|
+
applied,
|
|
198
|
+
"image",
|
|
199
|
+
persistTheme
|
|
200
|
+
? `已将「${applied.theme?.name || themeName}」设为背景。`
|
|
201
|
+
: "已将图片设为背景。",
|
|
202
|
+
);
|
|
127
203
|
},
|
|
128
204
|
|
|
129
205
|
async applyPreset(id, signal) {
|
|
@@ -132,6 +208,7 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
132
208
|
if (!preset || !imagePath) fail("未找到内置主题文件。");
|
|
133
209
|
const result = await this.applyImage(imagePath, signal, {
|
|
134
210
|
effects: effectsForPreset(id),
|
|
211
|
+
persistTheme: false,
|
|
135
212
|
});
|
|
136
213
|
try {
|
|
137
214
|
await this.setTone(preset.tone, signal);
|
|
@@ -167,7 +244,11 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
167
244
|
if (inspected.kind !== "video") {
|
|
168
245
|
fail("beauticode_apply_video 只接受 .mp4 文件。");
|
|
169
246
|
}
|
|
170
|
-
const
|
|
247
|
+
const persistTheme = input?.persistTheme !== false;
|
|
248
|
+
const themeName =
|
|
249
|
+
String(input?.themeName ?? "").trim() ||
|
|
250
|
+
themeNameFromFilePath(inspected.path, "视频");
|
|
251
|
+
const body = { videoPath: inspected.path, persistTheme, themeName };
|
|
171
252
|
const localInput = { type: "video", videoPath: inspected.path };
|
|
172
253
|
if (typeof input.poster === "string" && input.poster.trim()) {
|
|
173
254
|
const poster = await inspectLocalMedia(input.poster);
|
|
@@ -186,21 +267,29 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
186
267
|
}
|
|
187
268
|
const resolved = await backend();
|
|
188
269
|
if (resolved.kind === "tray") {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
270
|
+
const result = await request({
|
|
271
|
+
method: "POST",
|
|
272
|
+
path: "/apply/video",
|
|
273
|
+
body,
|
|
274
|
+
signal,
|
|
275
|
+
});
|
|
276
|
+
return unwrapApplyResult(
|
|
277
|
+
result,
|
|
196
278
|
"video",
|
|
197
|
-
|
|
279
|
+
persistTheme
|
|
280
|
+
? `已将「${result.theme?.name || themeName}」设为背景。`
|
|
281
|
+
: "已将视频设为背景。",
|
|
198
282
|
);
|
|
199
283
|
}
|
|
200
|
-
|
|
201
|
-
await resolved.session.
|
|
284
|
+
const applied = persistTheme
|
|
285
|
+
? await resolved.session.applyImportedMedia(localInput, { name: themeName })
|
|
286
|
+
: await resolved.session.apply(localInput);
|
|
287
|
+
return unwrapApplyResult(
|
|
288
|
+
applied,
|
|
202
289
|
"video",
|
|
203
|
-
|
|
290
|
+
persistTheme
|
|
291
|
+
? `已将「${applied.theme?.name || themeName}」设为背景。`
|
|
292
|
+
: "已将视频设为背景。",
|
|
204
293
|
);
|
|
205
294
|
},
|
|
206
295
|
|
|
@@ -311,6 +400,32 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
311
400
|
};
|
|
312
401
|
},
|
|
313
402
|
|
|
403
|
+
async deleteTheme(id, signal) {
|
|
404
|
+
const themeId = String(id ?? "").trim();
|
|
405
|
+
if (!themeId) fail("必须提供主题。");
|
|
406
|
+
const resolved = await backend();
|
|
407
|
+
if (resolved.kind === "tray") {
|
|
408
|
+
const result = await request({
|
|
409
|
+
method: "POST",
|
|
410
|
+
path: "/theme/delete",
|
|
411
|
+
body: { id: themeId },
|
|
412
|
+
signal,
|
|
413
|
+
timeoutMs: QUICK_TIMEOUT_MS,
|
|
414
|
+
});
|
|
415
|
+
if (!result || result.ok === false) {
|
|
416
|
+
fail(await chineseError(result?.error || "删除主题失败。"));
|
|
417
|
+
}
|
|
418
|
+
return { ok: true, deleted: true, message: "已删除主题。" };
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
const deleted = await resolved.session.deleteSavedTheme(themeId);
|
|
422
|
+
if (!deleted) fail("未找到已保存的主题。");
|
|
423
|
+
return { ok: true, deleted: true, message: "已删除主题。" };
|
|
424
|
+
} catch (error) {
|
|
425
|
+
fail(await chineseError(error));
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
|
|
314
429
|
async setFish(enabled, signal) {
|
|
315
430
|
const resolved = await backend();
|
|
316
431
|
const want = Boolean(enabled);
|
package/cli.js
CHANGED
|
@@ -46,9 +46,17 @@ function defaultDataRoot() {
|
|
|
46
46
|
return path.join(os.homedir(), ".beauticode");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function defaultDshHome() {
|
|
50
|
-
return process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
51
|
-
}
|
|
49
|
+
function defaultDshHome() {
|
|
50
|
+
return process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function defaultPluginHome(dshHome) {
|
|
54
|
+
return path.join(path.resolve(dshHome), "plugins", pluginName);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function legacyDefaultPluginHome() {
|
|
58
|
+
return path.join(defaultDataRoot(), "plugin");
|
|
59
|
+
}
|
|
52
60
|
|
|
53
61
|
function toFileUri(filePath) {
|
|
54
62
|
const full = path.resolve(filePath).replaceAll("\\", "/");
|
|
@@ -222,15 +230,34 @@ async function sameInstalledVersion(dest) {
|
|
|
222
230
|
}
|
|
223
231
|
}
|
|
224
232
|
|
|
225
|
-
async function copyPackage(dest) {
|
|
233
|
+
async function copyPackage(dest) {
|
|
226
234
|
if (await sameInstalledVersion(dest)) return false;
|
|
227
235
|
await fsp.rm(dest, { recursive: true, force: true });
|
|
228
236
|
await fsp.cp(here, dest, {
|
|
229
237
|
recursive: true,
|
|
230
238
|
filter: (source) => shouldCopy(source),
|
|
231
239
|
});
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function removeLegacyManagedPlugin(legacyHome, currentHome) {
|
|
244
|
+
if (!legacyHome) return false;
|
|
245
|
+
const legacy = path.resolve(legacyHome);
|
|
246
|
+
if (legacy === path.resolve(currentHome) || !fs.existsSync(legacy)) return false;
|
|
247
|
+
try {
|
|
248
|
+
const pkg = JSON.parse(await fsp.readFile(path.join(legacy, "package.json"), "utf8"));
|
|
249
|
+
if (
|
|
250
|
+
![pluginName, "@beauticode/dsh-plugin"].includes(pkg.name) ||
|
|
251
|
+
!fs.existsSync(path.join(legacy, "index.mjs"))
|
|
252
|
+
) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
await fsp.rm(legacy, { recursive: true, force: true });
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
234
261
|
|
|
235
262
|
async function ensureEngine(dest) {
|
|
236
263
|
const vendor = path.join(dest, "vendor", "adapter-dsh", "index.js");
|
|
@@ -320,7 +347,7 @@ async function removeWebPackageDep(webPackage) {
|
|
|
320
347
|
return true;
|
|
321
348
|
}
|
|
322
349
|
|
|
323
|
-
async function install(opts) {
|
|
350
|
+
async function install(opts) {
|
|
324
351
|
const dest = path.resolve(opts.pluginHome);
|
|
325
352
|
const dshHome = path.resolve(opts.dshHome);
|
|
326
353
|
const webProfile = path.join(dshHome, "profiles", "web");
|
|
@@ -340,21 +367,25 @@ async function install(opts) {
|
|
|
340
367
|
await linkPluginIntoProfile(webProfile, dest);
|
|
341
368
|
await ensureWebPackageDep(webPackage, dest);
|
|
342
369
|
await writePatch(webPatch, packageInsert());
|
|
343
|
-
if (fs.existsSync(homePatch)) {
|
|
370
|
+
if (fs.existsSync(homePatch)) {
|
|
344
371
|
const homeRaw = await fsp.readFile(homePatch, "utf8");
|
|
345
|
-
if (hasBridge(homeRaw)) await removePatch(homePatch);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
console.log(
|
|
349
|
-
console.log(
|
|
372
|
+
if (hasBridge(homeRaw)) await removePatch(homePatch);
|
|
373
|
+
}
|
|
374
|
+
const migrated = await removeLegacyManagedPlugin(opts.legacyPluginHome, dest);
|
|
375
|
+
console.log(`已写入 ${webPatch}`);
|
|
376
|
+
console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`);
|
|
377
|
+
if (migrated) console.log("已迁移 1.0.5 的旧插件目录,已保留主题数据。");
|
|
378
|
+
console.log("请自己运行:npx @deepseek-ai/dsh web");
|
|
350
379
|
return { dest, patch: webPatch };
|
|
351
380
|
}
|
|
352
381
|
|
|
353
|
-
await fsp.mkdir(dshHome, { recursive: true });
|
|
354
|
-
await writePatch(homePatch, fileUriInsert(toFileUri(indexFile)));
|
|
355
|
-
|
|
356
|
-
console.log(
|
|
357
|
-
console.log(
|
|
382
|
+
await fsp.mkdir(dshHome, { recursive: true });
|
|
383
|
+
await writePatch(homePatch, fileUriInsert(toFileUri(indexFile)));
|
|
384
|
+
const migrated = await removeLegacyManagedPlugin(opts.legacyPluginHome, dest);
|
|
385
|
+
console.log(`DSH web profile 还不存在,已写入 ${homePatch}`);
|
|
386
|
+
console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`);
|
|
387
|
+
if (migrated) console.log("已迁移 1.0.5 的旧插件目录,已保留主题数据。");
|
|
388
|
+
console.log("请自己运行:npx @deepseek-ai/dsh web");
|
|
358
389
|
return { dest, patch: homePatch };
|
|
359
390
|
}
|
|
360
391
|
|
|
@@ -386,11 +417,15 @@ async function uninstall(opts) {
|
|
|
386
417
|
return { removed };
|
|
387
418
|
}
|
|
388
419
|
|
|
389
|
-
export async function runCli(argv = process.argv.slice(2)) {
|
|
390
|
-
const dshHome = argValue(argv, "--dsh-home") || defaultDshHome();
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
const opts = {
|
|
420
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
421
|
+
const dshHome = argValue(argv, "--dsh-home") || defaultDshHome();
|
|
422
|
+
const requestedPluginHome = argValue(argv, "--plugin-home");
|
|
423
|
+
const pluginHome = requestedPluginHome || defaultPluginHome(dshHome);
|
|
424
|
+
const opts = {
|
|
425
|
+
dshHome,
|
|
426
|
+
pluginHome,
|
|
427
|
+
legacyPluginHome: requestedPluginHome ? null : legacyDefaultPluginHome(),
|
|
428
|
+
};
|
|
394
429
|
if (argv.includes("--remove")) return uninstall(opts);
|
|
395
430
|
return install(opts);
|
|
396
431
|
}
|
package/console.js
CHANGED
|
@@ -32,9 +32,13 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-toggle:hover{ba
|
|
|
32
32
|
#beauticode-console-pop .bc-theme-toggle{width:100%;text-align:left}
|
|
33
33
|
#beauticode-console-pop .bc-theme-list{margin:6px 0 0;max-height:160px;overflow:auto;border:1px solid rgba(255,255,255,.08);border-radius:12px;background:#232830;padding:4px}
|
|
34
34
|
body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-list{border-color:rgba(0,0,0,.08);background:#f4f6f8}
|
|
35
|
-
#beauticode-console-pop .bc-theme-
|
|
35
|
+
#beauticode-console-pop .bc-theme-row{display:flex;gap:4px;align-items:center}
|
|
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}
|
|
36
37
|
#beauticode-console-pop .bc-theme-item:hover{background:rgba(255,255,255,.08)}
|
|
37
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-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
|
+
#beauticode-console-pop .bc-theme-del:hover{background:rgba(255,255,255,.08);opacity:1}
|
|
41
|
+
body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-del:hover{background:rgba(0,0,0,.06)}
|
|
38
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}
|
|
39
43
|
#beauticode-console-file{display:none !important}
|
|
40
44
|
`;
|
|
@@ -154,7 +158,11 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back
|
|
|
154
158
|
? "图片"
|
|
155
159
|
: "无背景";
|
|
156
160
|
statusEl.textContent = label;
|
|
157
|
-
if (data.
|
|
161
|
+
if (typeof data.themeId === "string" && data.themeId) {
|
|
162
|
+
currentThemeId = data.themeId;
|
|
163
|
+
} else if (data.atmosphere === "gallery") {
|
|
164
|
+
currentThemeId = "builtin-gallery";
|
|
165
|
+
}
|
|
158
166
|
muted = data.muted !== false;
|
|
159
167
|
soundBtn.classList.toggle("on", !muted);
|
|
160
168
|
soundBtn.textContent = muted ? "声音" : "声音开";
|
|
@@ -168,10 +176,13 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back
|
|
|
168
176
|
}
|
|
169
177
|
themesBox.hidden = false;
|
|
170
178
|
themeList.innerHTML = themes
|
|
171
|
-
.map(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
.map((theme) => {
|
|
180
|
+
const del =
|
|
181
|
+
theme.bundled === true
|
|
182
|
+
? ""
|
|
183
|
+
: `<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
|
+
return `<div class="bc-theme-row"><button type="button" class="bc-theme-item" data-theme-id="${escapeAttr(theme.id)}">${escapeText(theme.name)}</button>${del}</div>`;
|
|
185
|
+
})
|
|
175
186
|
.join("");
|
|
176
187
|
const selected = themes.find((theme) => theme.id === currentThemeId);
|
|
177
188
|
themeToggle.textContent = selected ? selected.name : "已保存主题";
|
|
@@ -210,22 +221,46 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back
|
|
|
210
221
|
}
|
|
211
222
|
}
|
|
212
223
|
|
|
213
|
-
async function run(task) {
|
|
224
|
+
async function run(task) {
|
|
214
225
|
if (busy) return;
|
|
215
226
|
busy = true;
|
|
216
|
-
for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item")) button.disabled = true;
|
|
227
|
+
for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item, .bc-theme-del")) button.disabled = true;
|
|
217
228
|
showMessage("");
|
|
218
229
|
try {
|
|
219
230
|
const result = await task();
|
|
231
|
+
if (result?.theme?.id) currentThemeId = result.theme.id;
|
|
220
232
|
if (result?.message) showMessage(result.message);
|
|
221
233
|
await refresh();
|
|
222
234
|
} catch (error) {
|
|
223
235
|
showMessage(error instanceof Error ? error.message : String(error));
|
|
224
236
|
} finally {
|
|
225
237
|
busy = false;
|
|
226
|
-
for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item")) button.disabled = false;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
238
|
+
for (const button of pop.querySelectorAll(".bc-btn, .bc-theme-toggle, .bc-theme-item, .bc-theme-del")) button.disabled = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function askThemeName(fileName) {
|
|
243
|
+
const suggested = String(fileName || "")
|
|
244
|
+
.replace(/\.[^.]+$/, "")
|
|
245
|
+
.trim()
|
|
246
|
+
.slice(0, 80);
|
|
247
|
+
const entered = window.prompt("请给该主题取个名字(1–80 个字符):", suggested);
|
|
248
|
+
if (entered == null) return null;
|
|
249
|
+
const name = entered.trim();
|
|
250
|
+
if (!name) {
|
|
251
|
+
showMessage("主题名不能为空。");
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
if (name.length > 80) {
|
|
255
|
+
showMessage("主题名不能超过 80 个字符。");
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
if (/[<>:"/\\|?*]/.test(name) || /[\u0000-\u001f]/.test(name)) {
|
|
259
|
+
showMessage('主题名不能包含 < > : " / \\ | ? * 或控制字符。');
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return name;
|
|
263
|
+
}
|
|
229
264
|
|
|
230
265
|
trigger.addEventListener("click", (event) => {
|
|
231
266
|
event.stopPropagation();
|
|
@@ -268,6 +303,23 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back
|
|
|
268
303
|
themeToggle.setAttribute("aria-expanded", open ? "true" : "false");
|
|
269
304
|
});
|
|
270
305
|
themeList.addEventListener("click", (event) => {
|
|
306
|
+
const del = event.target.closest("[data-theme-delete]");
|
|
307
|
+
if (del) {
|
|
308
|
+
event.stopPropagation();
|
|
309
|
+
const id = del.getAttribute("data-theme-delete") || "";
|
|
310
|
+
const name = del.getAttribute("data-theme-name") || "主题";
|
|
311
|
+
if (!id) return;
|
|
312
|
+
if (!window.confirm(`确定删除主题「${name}」?`)) return;
|
|
313
|
+
if (currentThemeId === id) currentThemeId = "";
|
|
314
|
+
void run(() =>
|
|
315
|
+
request("/__beauticode/ui/theme/delete", {
|
|
316
|
+
method: "POST",
|
|
317
|
+
headers: { "content-type": "application/json" },
|
|
318
|
+
body: JSON.stringify({ id }),
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
271
323
|
const item = event.target.closest("[data-theme-id]");
|
|
272
324
|
if (!item) return;
|
|
273
325
|
currentThemeId = item.getAttribute("data-theme-id") || "";
|
|
@@ -284,17 +336,21 @@ body:not([data-ds-dark-theme]) #beauticode-console-pop .bc-theme-item:hover{back
|
|
|
284
336
|
}),
|
|
285
337
|
);
|
|
286
338
|
});
|
|
287
|
-
fileInput.addEventListener("change", () => {
|
|
288
|
-
const file = fileInput.files?.[0];
|
|
289
|
-
fileInput.value = "";
|
|
290
|
-
if (!file) return;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
339
|
+
fileInput.addEventListener("change", () => {
|
|
340
|
+
const file = fileInput.files?.[0];
|
|
341
|
+
fileInput.value = "";
|
|
342
|
+
if (!file) return;
|
|
343
|
+
const themeName = askThemeName(file.name);
|
|
344
|
+
if (!themeName) return;
|
|
345
|
+
void run(() =>
|
|
346
|
+
request("/__beauticode/ui/import", {
|
|
347
|
+
method: "POST",
|
|
348
|
+
headers: {
|
|
349
|
+
"x-beauticode-filename": encodeURIComponent(file.name),
|
|
350
|
+
"x-beauticode-theme-name": encodeURIComponent(themeName),
|
|
351
|
+
},
|
|
352
|
+
body: file,
|
|
353
|
+
}),
|
|
298
354
|
);
|
|
299
355
|
});
|
|
300
356
|
|
package/index.mjs
CHANGED
|
@@ -376,6 +376,11 @@ export function apply(ctx, config = {}) {
|
|
|
376
376
|
path: "/__beauticode/ui/theme/use",
|
|
377
377
|
handler: (req, res) => ui.useTheme(req, res),
|
|
378
378
|
}),
|
|
379
|
+
ctx.webServer.register({
|
|
380
|
+
kind: "exact",
|
|
381
|
+
path: "/__beauticode/ui/theme/delete",
|
|
382
|
+
handler: (req, res) => ui.deleteTheme(req, res),
|
|
383
|
+
}),
|
|
379
384
|
ctx.webServer.register({
|
|
380
385
|
kind: "exact",
|
|
381
386
|
path: "/__beauticode/ui/preset",
|
package/package.json
CHANGED
package/ui-host.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
6
|
import { Transform } from "node:stream";
|
|
7
|
-
import { createBeauticodeActions } from "./agent.mjs";
|
|
7
|
+
import { createBeauticodeActions } from "./agent.mjs";
|
|
8
8
|
import { callDshControl } from "./control-client.mjs";
|
|
9
9
|
import { createGalleryHandlers, resolveConfiguredSkinCenterUrl } from "./gallery-host.mjs";
|
|
10
10
|
import { hasLiveTray, resolveApplyBackend, stopInProcessSession } from "./host-apply.mjs";
|
|
@@ -13,7 +13,7 @@ const IMAGE_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".webp", ".avif"]);
|
|
|
13
13
|
const MAX_IMAGE_BYTES = 18 * 1024 * 1024;
|
|
14
14
|
const MAX_VIDEO_BYTES = 800 * 1024 * 1024;
|
|
15
15
|
|
|
16
|
-
export function parseImportFilename(raw) {
|
|
16
|
+
export function parseImportFilename(raw) {
|
|
17
17
|
if (typeof raw !== "string" || !raw.trim()) {
|
|
18
18
|
return { ok: false, error: "缺少文件名。" };
|
|
19
19
|
}
|
|
@@ -35,7 +35,29 @@ export function parseImportFilename(raw) {
|
|
|
35
35
|
return { ok: true, name, ext, kind: "image", maxBytes: MAX_IMAGE_BYTES };
|
|
36
36
|
}
|
|
37
37
|
return { ok: false, error: "只支持图片(jpg / jpeg / png / webp / avif)或 MP4 视频。" };
|
|
38
|
-
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseImportThemeName(raw) {
|
|
41
|
+
if (Array.isArray(raw)) raw = raw[0];
|
|
42
|
+
if (typeof raw !== "string" || !raw.trim()) {
|
|
43
|
+
return { ok: false, error: "请先给该主题取名。" };
|
|
44
|
+
}
|
|
45
|
+
let decoded = raw.trim();
|
|
46
|
+
try {
|
|
47
|
+
decoded = decodeURIComponent(decoded);
|
|
48
|
+
} catch {
|
|
49
|
+
return { ok: false, error: "主题名编码无效。" };
|
|
50
|
+
}
|
|
51
|
+
const name = decoded.trim();
|
|
52
|
+
if (!name) return { ok: false, error: "主题名不能为空。" };
|
|
53
|
+
if (name.length > 80) {
|
|
54
|
+
return { ok: false, error: "主题名不能超过 80 个字符。" };
|
|
55
|
+
}
|
|
56
|
+
if (/[<>:"/\\|?*]/.test(name) || /[\u0000-\u001f]/.test(name)) {
|
|
57
|
+
return { ok: false, error: "主题名包含非法字符。" };
|
|
58
|
+
}
|
|
59
|
+
return { ok: true, name };
|
|
60
|
+
}
|
|
39
61
|
|
|
40
62
|
function limitBytes(maxBytes) {
|
|
41
63
|
let size = 0;
|
|
@@ -146,6 +168,7 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
|
|
|
146
168
|
media: background?.type ?? null,
|
|
147
169
|
muted: status.muted !== false,
|
|
148
170
|
atmosphere: background?.effects?.preset ?? null,
|
|
171
|
+
themeId: status.themeId || null,
|
|
149
172
|
themes: publicThemes(listed.themes),
|
|
150
173
|
message: status.message,
|
|
151
174
|
skinCenter: { url: center, enabled: Boolean(center) },
|
|
@@ -167,13 +190,21 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
|
|
|
167
190
|
res.writeHead(403).end();
|
|
168
191
|
return;
|
|
169
192
|
}
|
|
170
|
-
const parsed = parseImportFilename(req.headers["x-beauticode-filename"]);
|
|
193
|
+
const parsed = parseImportFilename(req.headers["x-beauticode-filename"]);
|
|
171
194
|
if (!parsed.ok) {
|
|
172
195
|
req.resume();
|
|
173
196
|
sendJson(res, 400, { ok: false, error: parsed.error });
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const parsedTheme = parseImportThemeName(
|
|
200
|
+
req.headers["x-beauticode-theme-name"],
|
|
201
|
+
);
|
|
202
|
+
if (!parsedTheme.ok) {
|
|
203
|
+
req.resume();
|
|
204
|
+
sendJson(res, 400, { ok: false, error: parsedTheme.error });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const length = Number(req.headers["content-length"]);
|
|
177
208
|
if (Number.isFinite(length) && length > parsed.maxBytes) {
|
|
178
209
|
req.resume();
|
|
179
210
|
sendJson(res, 413, { ok: false, error: "文件过大。" });
|
|
@@ -187,10 +218,11 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
|
|
|
187
218
|
);
|
|
188
219
|
try {
|
|
189
220
|
await pipeline(req, limitBytes(parsed.maxBytes), fs.createWriteStream(tmpPath));
|
|
221
|
+
const themeName = parsedTheme.name;
|
|
190
222
|
const result =
|
|
191
223
|
parsed.kind === "video"
|
|
192
|
-
? await actions.applyVideo({ path: tmpPath })
|
|
193
|
-
: await actions.applyImage(tmpPath);
|
|
224
|
+
? await actions.applyVideo({ path: tmpPath, themeName })
|
|
225
|
+
: await actions.applyImage(tmpPath, undefined, { themeName });
|
|
194
226
|
sendJson(res, 200, result);
|
|
195
227
|
} catch (error) {
|
|
196
228
|
const status = error?.statusCode === 413 ? 413 : 422;
|
|
@@ -270,6 +302,30 @@ export function createBeauticodeUi({ dataRoot, baseUrl, getBaseUrl, sendJson, is
|
|
|
270
302
|
}
|
|
271
303
|
},
|
|
272
304
|
|
|
305
|
+
async deleteTheme(req, res) {
|
|
306
|
+
if (req.method !== "POST") {
|
|
307
|
+
res.writeHead(405).end();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!isSameOrigin(req)) {
|
|
311
|
+
res.writeHead(403).end();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const body = await readJson(req);
|
|
315
|
+
if (typeof body.id !== "string" || !body.id.trim()) {
|
|
316
|
+
sendJson(res, 400, { ok: false, error: "必须提供主题。" });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
sendJson(res, 200, await actions.deleteTheme(body.id.trim()));
|
|
321
|
+
} catch (error) {
|
|
322
|
+
sendJson(res, 422, {
|
|
323
|
+
ok: false,
|
|
324
|
+
error: error instanceof Error ? error.message : String(error),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
|
|
273
329
|
async galleryConfig(req, res) {
|
|
274
330
|
return gallery.config(req, res, sendJson, isSameOrigin);
|
|
275
331
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { ApplyTransaction, BackgroundStore, MediaServerController, buildHostApplyPayload, defaultDataRoot, resolveSessionBundledThemes, } from "../core/index.js";
|
|
3
|
+
import { ApplyTransaction, BackgroundStore, MediaServerController, buildHostApplyPayload, defaultDataRoot, persistAndApplyImportedMedia, resolveSessionBundledThemes, } from "../core/index.js";
|
|
4
4
|
import { DshHostApplier, normalizeDshBaseUrl } from "./bridge.js";
|
|
5
5
|
import { DSH_HOST_DESCRIPTOR } from "./host-descriptor.js";
|
|
6
6
|
import { acquireDshInjectorLock } from "./injector-lock.js";
|
|
@@ -93,7 +93,10 @@ export class DshSession {
|
|
|
93
93
|
this.startHandoffLoop();
|
|
94
94
|
void this.yieldToTrayIfRequested();
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
// Finish the initial restore check before accepting the first user apply.
|
|
97
|
+
// Otherwise a click immediately after lazy session startup can race the
|
|
98
|
+
// background reapply and be rejected as a concurrent operation.
|
|
99
|
+
await this.watchOnce();
|
|
97
100
|
return { port: this.bridgePort() };
|
|
98
101
|
}
|
|
99
102
|
async apply(input) {
|
|
@@ -258,6 +261,7 @@ export class DshSession {
|
|
|
258
261
|
fish: this.fishMode,
|
|
259
262
|
muted: this.videoMuted,
|
|
260
263
|
tone: this.backgroundTone,
|
|
264
|
+
themeId: this.activeThemeId,
|
|
261
265
|
};
|
|
262
266
|
}
|
|
263
267
|
async saveCurrentTheme(name) {
|
|
@@ -274,18 +278,16 @@ export class DshSession {
|
|
|
274
278
|
}
|
|
275
279
|
}
|
|
276
280
|
const theme = await this.store.saveCurrentTheme(name, { videoPositionSec });
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
this.lastProgressWriteSec = -1;
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
this.activeThemeId = null;
|
|
283
|
-
}
|
|
281
|
+
this.activeThemeId = theme.id;
|
|
282
|
+
this.lastProgressWriteSec = -1;
|
|
284
283
|
return theme;
|
|
285
284
|
}
|
|
286
285
|
async importSavedTheme(input) {
|
|
287
286
|
return this.store.importSavedThemeFromMedia(input);
|
|
288
287
|
}
|
|
288
|
+
async applyImportedMedia(input, opts = {}) {
|
|
289
|
+
return this.trackOperation(persistAndApplyImportedMedia(this, input, opts));
|
|
290
|
+
}
|
|
289
291
|
async listSavedThemes() {
|
|
290
292
|
return this.store.listSavedThemes();
|
|
291
293
|
}
|
|
@@ -302,7 +304,7 @@ export class DshSession {
|
|
|
302
304
|
const saved = await this.store.loadSavedTheme(themeId);
|
|
303
305
|
const result = await this.apply(saved.input);
|
|
304
306
|
if (result.ok) {
|
|
305
|
-
this.activeThemeId = saved.
|
|
307
|
+
this.activeThemeId = saved.themeId;
|
|
306
308
|
this.lastProgressWriteSec = -1;
|
|
307
309
|
}
|
|
308
310
|
return result;
|
|
@@ -707,15 +707,32 @@ export class BackgroundStore {
|
|
|
707
707
|
if (existing)
|
|
708
708
|
return existing;
|
|
709
709
|
}
|
|
710
|
-
const
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
710
|
+
const imagePath = String(input.imagePath ?? "").trim();
|
|
711
|
+
const videoPath = String(input.videoPath ?? "").trim();
|
|
712
|
+
if (!imagePath && !videoPath) {
|
|
713
|
+
throw new MediaValidationError("Import requires an image or video file.");
|
|
714
|
+
}
|
|
715
|
+
const video = videoPath ? await validateVideoFile(videoPath) : null;
|
|
716
|
+
let image = null;
|
|
717
|
+
if (imagePath) {
|
|
718
|
+
image = await validateImageFile(imagePath);
|
|
719
|
+
}
|
|
720
|
+
else if (video) {
|
|
721
|
+
try {
|
|
722
|
+
const previous = await this.readActiveManifest();
|
|
723
|
+
if (previous.background?.image) {
|
|
724
|
+
image = await validateImageFile(path.join(this.paths.activeDir, previous.background.image));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
image = null;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
714
731
|
const usage = await this.#savedThemeUsage();
|
|
715
732
|
if (usage.count >= this.maxSavedThemes) {
|
|
716
733
|
throw new MediaValidationError(`Saved theme limit reached (${this.maxSavedThemes}). Delete one before saving.`);
|
|
717
734
|
}
|
|
718
|
-
const incomingBytes = image
|
|
735
|
+
const incomingBytes = (image?.size ?? SYNTHETIC_POSTER_PNG.length) + (video?.size ?? 0);
|
|
719
736
|
if (usage.bytes + incomingBytes > this.maxSavedBytes) {
|
|
720
737
|
throw new MediaValidationError(`Saved theme storage limit exceeded (${this.maxSavedBytes} bytes). Delete a theme before saving.`);
|
|
721
738
|
}
|
|
@@ -726,9 +743,17 @@ export class BackgroundStore {
|
|
|
726
743
|
}
|
|
727
744
|
const stagingDir = await this.#createStagingDir();
|
|
728
745
|
try {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
746
|
+
let imageBasename;
|
|
747
|
+
if (image) {
|
|
748
|
+
imageBasename = `poster${image.extension === ".jpeg" ? ".jpg" : image.extension}`;
|
|
749
|
+
assertSafeBasename(imageBasename, "image");
|
|
750
|
+
await copyFileAtomic(image.filePath, path.join(stagingDir, imageBasename));
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
imageBasename = "poster.png";
|
|
754
|
+
assertSafeBasename(imageBasename, "image");
|
|
755
|
+
await fs.writeFile(path.join(stagingDir, imageBasename), SYNTHETIC_POSTER_PNG);
|
|
756
|
+
}
|
|
732
757
|
const background = video
|
|
733
758
|
? {
|
|
734
759
|
type: "video",
|
|
@@ -760,6 +785,13 @@ export class BackgroundStore {
|
|
|
760
785
|
};
|
|
761
786
|
if (source)
|
|
762
787
|
meta.source = source;
|
|
788
|
+
if (video) {
|
|
789
|
+
const pos = normalizeVideoPositionSec(input.videoPositionSec);
|
|
790
|
+
if (pos != null) {
|
|
791
|
+
meta.videoPositionSec = pos;
|
|
792
|
+
meta.videoPositionUpdatedAt = nowIso();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
763
795
|
await this.#writeJsonAtomic(path.join(stagingDir, SAVED_META_NAME), meta);
|
|
764
796
|
await this.#validateTree(stagingDir, savedManifest);
|
|
765
797
|
await fs.rename(stagingDir, dir);
|
|
@@ -1194,6 +1226,70 @@ export class BackgroundStore {
|
|
|
1194
1226
|
return { count, bytes };
|
|
1195
1227
|
}
|
|
1196
1228
|
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Display name for an imported file. Id stays a random ASCII folder; this is
|
|
1231
|
+
* only the label shown in 已保存主题.
|
|
1232
|
+
*/
|
|
1233
|
+
export function themeNameFromFilePath(filePath, fallback = "主题") {
|
|
1234
|
+
const base = path.basename(String(filePath ?? "").replaceAll("\\", "/"));
|
|
1235
|
+
const ext = path.extname(base);
|
|
1236
|
+
let name = (ext ? base.slice(0, -ext.length) : base).trim();
|
|
1237
|
+
name = name
|
|
1238
|
+
.replace(/[<>:"/\\|?*]/g, " ")
|
|
1239
|
+
.replace(/[\u0000-\u001f]/g, "")
|
|
1240
|
+
.replace(/\s+/g, " ")
|
|
1241
|
+
.trim();
|
|
1242
|
+
if (!name)
|
|
1243
|
+
name = fallback;
|
|
1244
|
+
if (name.length > 80)
|
|
1245
|
+
name = name.slice(0, 80).trim();
|
|
1246
|
+
if (!name)
|
|
1247
|
+
name = fallback;
|
|
1248
|
+
return name;
|
|
1249
|
+
}
|
|
1250
|
+
export function importedThemeInputFromApply(input, name) {
|
|
1251
|
+
if (input.type === "image") {
|
|
1252
|
+
const next = {
|
|
1253
|
+
name: String(name ?? "").trim() || themeNameFromFilePath(input.imagePath, "图片"),
|
|
1254
|
+
imagePath: input.imagePath,
|
|
1255
|
+
};
|
|
1256
|
+
if (input.effects)
|
|
1257
|
+
next.effects = input.effects;
|
|
1258
|
+
return next;
|
|
1259
|
+
}
|
|
1260
|
+
const next = {
|
|
1261
|
+
name: String(name ?? "").trim() || themeNameFromFilePath(input.videoPath, "视频"),
|
|
1262
|
+
videoPath: input.videoPath,
|
|
1263
|
+
};
|
|
1264
|
+
if (input.imagePath)
|
|
1265
|
+
next.imagePath = input.imagePath;
|
|
1266
|
+
const pos = normalizeVideoPositionSec(input.startAt);
|
|
1267
|
+
if (pos != null)
|
|
1268
|
+
next.videoPositionSec = pos;
|
|
1269
|
+
return next;
|
|
1270
|
+
}
|
|
1271
|
+
export async function persistAndApplyImportedMedia(session, input, opts = {}) {
|
|
1272
|
+
if (input.type !== "image" && input.type !== "video") {
|
|
1273
|
+
return {
|
|
1274
|
+
ok: false,
|
|
1275
|
+
error: "Imported media must be an image or video.",
|
|
1276
|
+
rolledBack: false,
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
let theme;
|
|
1280
|
+
try {
|
|
1281
|
+
theme = await session.importSavedTheme(importedThemeInputFromApply(input, opts.name));
|
|
1282
|
+
}
|
|
1283
|
+
catch (error) {
|
|
1284
|
+
return {
|
|
1285
|
+
ok: false,
|
|
1286
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1287
|
+
rolledBack: false,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
const result = await session.useSavedTheme(theme.id);
|
|
1291
|
+
return { ...result, theme };
|
|
1292
|
+
}
|
|
1197
1293
|
/** Normalize a playback position for theme meta. Invalid → null (caller treats as 0). */
|
|
1198
1294
|
export function normalizeVideoPositionSec(value) {
|
|
1199
1295
|
const n = typeof value === "number" ? value : Number(value);
|
|
@@ -126,6 +126,8 @@ export function toChineseErrorMessage(value) {
|
|
|
126
126
|
[/No active background to save\.?/i, "当前没有可保存的背景。"],
|
|
127
127
|
[/Saved theme limit reached \((\d+)\)\. Delete one before saving\.?/i, (match) => `已达到保存主题数量上限(${match[1]}),请先删除一个主题。`],
|
|
128
128
|
[/Saved theme storage limit exceeded \((\d+) bytes\)\. Delete a theme before saving\.?/i, (match) => `已超过保存主题的存储上限(${match[1]} 字节),请先删除一个主题。`],
|
|
129
|
+
[/Import requires an image or video file\.?/i, "导入需要图片或视频文件。"],
|
|
130
|
+
[/Imported media must be an image or video\.?/i, "导入的媒体必须是图片或视频。"],
|
|
129
131
|
[/Built-in theme cannot be deleted\.?/i, "内置主题不能删除。"],
|
|
130
132
|
[/Invalid saved theme id\.?/i, "已保存主题 ID 无效。"],
|
|
131
133
|
[/Invalid skin-center media path\.?/i, "皮肤中心地址无效。"],
|
package/vendor/core/paths.js
CHANGED
|
@@ -68,6 +68,7 @@ export async function ensureDataLayout(paths) {
|
|
|
68
68
|
entry === SNAPSHOTS_DIR_NAME ||
|
|
69
69
|
entry === SAVED_DIR_NAME ||
|
|
70
70
|
entry === RUNTIME_MEDIA_DIR_NAME ||
|
|
71
|
+
entry === "tmp" ||
|
|
71
72
|
entry === "logs" ||
|
|
72
73
|
entry === "engine-launcher.log" ||
|
|
73
74
|
entry === "injector.lock" ||
|