dsh-boot-suite 2.13.21 → 2.15.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +113 -0
- package/README.md +9 -2
- package/lib/client.js +445 -23
- package/lib/host-startup.js +196 -3
- package/lib/host-wallpaper.js +32 -11
- package/lib/splash.js +270 -0
- package/package.json +2 -2
- package/templates/division-bigfish-codex/app.js +255 -0
- package/templates/division-bigfish-codex/assets/egg-bg.png +0 -0
- package/templates/division-bigfish-codex/assets/voice-full-plus.wav +0 -0
- package/templates/division-bigfish-codex/assets/voice-line1.wav +0 -0
- package/templates/division-bigfish-codex/assets/voice-line2.wav +0 -0
- package/templates/division-bigfish-codex/assets/voice-line3.wav +0 -0
- package/templates/division-bigfish-codex/assets/voice-line4.wav +0 -0
- package/templates/division-bigfish-codex/assets/voice-orig1.wav +0 -0
- package/templates/division-bigfish-codex/assets/voice-orig3.wav +0 -0
- package/templates/division-bigfish-codex/assets/voice-reconnect.wav +0 -0
- package/templates/division-bigfish-codex/assets/whale-badge-line.png +0 -0
- package/templates/division-bigfish-codex/config.js +24 -0
- package/templates/division-bigfish-codex/index.html +1 -0
- package/templates/division-bigfish-codex/motion.js +30 -0
- package/templates/division-bigfish-codex/preview-easteregg.mp4 +0 -0
- package/templates/division-bigfish-codex/preview-normal.mp4 +0 -0
- package/templates/division-bigfish-codex/render.html +1 -0
- package/templates/division-bigfish-codex/style.css +18 -0
- package/templates/division-bigfish-codex/template.json +10 -0
- package/templates/division-bigfish-codex/voice-env.js +1 -0
- package/templates/division-bigfish-codex/voice.js +10 -0
- package/templates/division-bigfish-codex//344/272/244/346/216/245-Codex/347/211/210/346/216/245/345/205/245/346/226/271/346/241/210.md +207 -0
- package/templates/division-bigfish-codex//346/217/222/344/273/266/346/216/245/345/205/245/345/245/221/347/272/246.md +177 -0
- package/test/smoke-client.mjs +4 -4
- package/test/smoke-host.mjs +23 -1
- package/test/smoke-package.mjs +89 -1
- package/test/smoke-splash.mjs +2 -0
- package/test/smoke-wallpaper-client.mjs +1 -1
- package/test/smoke-wallpaper-host.mjs +7 -1
- package/tools/README.md +1 -1
- package/tools/install.ps1 +7 -4
- package/tools/make-upgrade-helper.mjs +20 -0
- package/tools/stage-local.mjs +158 -0
package/lib/host-startup.js
CHANGED
|
@@ -91,6 +91,13 @@ const DEFAULTS = {
|
|
|
91
91
|
voiceFx: 'robot', // 声音效果:none | ai-mech | robot | radio | oldtv | dark(默认机器人,贴近旧版听感)
|
|
92
92
|
voiceLead: 240, // 提示音之后留一小段空白再开口
|
|
93
93
|
welcomeDelay: 550, // 05→06 过场和声钟之后等多久再进终幕欢迎语(0–3000ms;实测 550 最顺)
|
|
94
|
+
/* v2.15.0:iframe 启动模板(division-bigfish-codex)专属文案覆盖;空串 = 用模板自带的 config.js */
|
|
95
|
+
bigfishUsername: '',
|
|
96
|
+
bigfishTitle: '',
|
|
97
|
+
bigfishDesc: '',
|
|
98
|
+
bigfishForceEgg: false, // 仅下一次启动:强制判定掉线彩蛋(便于测试)
|
|
99
|
+
bigfishVoiceFx: 'none', // 全景大肥鱼模板内五句配音的效果:none|ai-mech|robot|radio|oldtv|dark
|
|
100
|
+
bigfishVoiceVolume: 50, // 全景大肥鱼模板内五句配音的音量(0–100,默认 50%)
|
|
94
101
|
identity: 'JOYCE MOORE', // ← 设置页里可改的「身份名称」
|
|
95
102
|
identityId: '0087',
|
|
96
103
|
accessLevel: '3',
|
|
@@ -115,6 +122,26 @@ const DEFAULTS = {
|
|
|
115
122
|
transitionMs: 600,
|
|
116
123
|
transitionDir: 'left', // 仅 scanline / slide 使用:left|right|up|down
|
|
117
124
|
hiddenTemplates: [], // v2.12.4:被用户「隐藏」的内置模板 id 列表(本地模板删除走文件)
|
|
125
|
+
testTemplates: [], // v2.14.0:被归到「测试类模板」的模板 id 列表(正式下拉不再显示)
|
|
126
|
+
promotedTemplates: [], // v2.14.0:被用户从测试类放回正式列表的模板 id 列表
|
|
127
|
+
/* v2.14.0 用户自备 BGM:只读本机文件、不上传、不内置。三段 = 第一幕 / 第二幕(含终幕) / 结尾淡出 */
|
|
128
|
+
bgmEnabled: false,
|
|
129
|
+
bgmLoop: true, // 整体循环;关掉后拍速/拍数/选段不再参与,整首只播一遍
|
|
130
|
+
bgmFile: '', // 主音乐(存于 ~/.dsh/startup-music 的文件名)
|
|
131
|
+
bgmFile2: '', // 第二段换曲;'' = 同主音乐
|
|
132
|
+
bgmVolume: 0.5,
|
|
133
|
+
bgmBpm: 100, // 拍速(只用于算选段时长)
|
|
134
|
+
bgmBeats1: 16, // 第一段拍数(选段时长 = 拍数 * 60 / BPM)
|
|
135
|
+
bgmBeats2: 32, // 第二段拍数
|
|
136
|
+
bgm1Start: 0, // 第一段在音频中的起点(秒)
|
|
137
|
+
bgm2Start: 0, // 第二段起点(秒)
|
|
138
|
+
bgm1FadeIn: 1, // 第一段淡入(秒)
|
|
139
|
+
bgm1FadeOut: 1, // 第一段淡出(到第二段前,秒)
|
|
140
|
+
bgm2FadeIn: 1, // 第二段淡入(秒)
|
|
141
|
+
bgm2FadeOut: 2, // 第二段/结束淡出(秒)
|
|
142
|
+
bgmCrossfade: 1200, // 两段交叉时长(毫秒)
|
|
143
|
+
bgm2Loop: false, // 第二段是否循环
|
|
144
|
+
bgmDuck: true // 配音时自动降低音乐(闪避)
|
|
118
145
|
}
|
|
119
146
|
|
|
120
147
|
const TEXT_KEYS = [
|
|
@@ -139,7 +166,7 @@ const ENUMS = {
|
|
|
139
166
|
transitionOut: TRANSITION_TYPES,
|
|
140
167
|
transitionDir: TRANSITION_DIRS
|
|
141
168
|
}
|
|
142
|
-
const BOOL_KEYS = ['enabled', 'requireInteraction', 'allowSkip', 'sound', 'bedNoise', 'voice', 'voiceCue']
|
|
169
|
+
const BOOL_KEYS = ['enabled', 'requireInteraction', 'allowSkip', 'sound', 'bedNoise', 'voice', 'voiceCue', 'bgmEnabled', 'bgmLoop', 'bgm2Loop', 'bgmDuck']
|
|
143
170
|
/** 数值字段的合法区间(越界即夹取)。 */
|
|
144
171
|
const RANGES = {
|
|
145
172
|
welcomeDelay: [0, 3000],
|
|
@@ -149,7 +176,19 @@ const RANGES = {
|
|
|
149
176
|
voiceRate: [0.5, 2],
|
|
150
177
|
voiceLead: [0, 1200],
|
|
151
178
|
voicePitch: [0, 2],
|
|
152
|
-
voiceVolume: [0, 1]
|
|
179
|
+
voiceVolume: [0, 1],
|
|
180
|
+
bgmVolume: [0, 1],
|
|
181
|
+
bgmBpm: [40, 220],
|
|
182
|
+
bgmBeats1: [4, 128],
|
|
183
|
+
bgmBeats2: [4, 128],
|
|
184
|
+
bgm1Start: [0, 7200],
|
|
185
|
+
bgm2Start: [0, 7200],
|
|
186
|
+
bgm1FadeIn: [0, 10],
|
|
187
|
+
bgm1FadeOut: [0, 10],
|
|
188
|
+
bgm2FadeIn: [0, 10],
|
|
189
|
+
bgm2FadeOut: [0, 10],
|
|
190
|
+
bgmCrossfade: [0, 5000],
|
|
191
|
+
bigfishVoiceVolume: [0, 100]
|
|
153
192
|
}
|
|
154
193
|
|
|
155
194
|
function normalize(input) {
|
|
@@ -175,8 +214,15 @@ function normalize(input) {
|
|
|
175
214
|
out[key] = value
|
|
176
215
|
}
|
|
177
216
|
}
|
|
217
|
+
const tplIdOk = (x) => typeof x === 'string' && /^[a-z0-9][a-z0-9-]{0,39}$/.test(x)
|
|
178
218
|
out.hiddenTemplates = Array.isArray(src.hiddenTemplates)
|
|
179
|
-
? [...new Set(src.hiddenTemplates.filter(
|
|
219
|
+
? [...new Set(src.hiddenTemplates.filter(tplIdOk))].slice(0, 60)
|
|
220
|
+
: []
|
|
221
|
+
out.testTemplates = Array.isArray(src.testTemplates)
|
|
222
|
+
? [...new Set(src.testTemplates.filter(tplIdOk))].slice(0, 200)
|
|
223
|
+
: []
|
|
224
|
+
out.promotedTemplates = Array.isArray(src.promotedTemplates)
|
|
225
|
+
? [...new Set(src.promotedTemplates.filter(tplIdOk))].slice(0, 200)
|
|
180
226
|
: []
|
|
181
227
|
if (!String(out.identity).trim()) out.identity = DEFAULTS.identity
|
|
182
228
|
if (!String(out.identityId).trim()) {
|
|
@@ -577,6 +623,57 @@ function apply(ctx) {
|
|
|
577
623
|
]
|
|
578
624
|
const TEMPLATE_ID_RE = /^[a-z0-9][a-z0-9-]{0,39}$/
|
|
579
625
|
|
|
626
|
+
/* v2.15.0:iframe 启动模板的静态文件路由。
|
|
627
|
+
/dsh-startup/template-file/<模板id>/<相对路径>:只放行白名单扩展名,禁止 .. 和绝对路径。
|
|
628
|
+
division-bigfish-codex/config.js 会追加设置页里的「连接用户名 / 标题 / 正文」覆盖。 */
|
|
629
|
+
const TEMPLATE_FILE_EXT_RE = /\.(html|js|css|json|wav|mp3|ogg|m4a|aac|png|jpe?g|webp|gif|svg|mp4|webm|woff2?|ttf|txt)$/i
|
|
630
|
+
const TEMPLATE_FILE_SEG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
631
|
+
const TEMPLATE_FILE_TYPES = {
|
|
632
|
+
'.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
|
|
633
|
+
'.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8',
|
|
634
|
+
'.wav': 'audio/wav', '.mp3': 'audio/mpeg', '.ogg': 'audio/ogg', '.m4a': 'audio/mp4', '.aac': 'audio/aac',
|
|
635
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.gif': 'image/gif',
|
|
636
|
+
'.svg': 'image/svg+xml', '.mp4': 'video/mp4', '.webm': 'video/webm',
|
|
637
|
+
'.woff2': 'font/woff2', '.woff': 'font/woff', '.ttf': 'font/ttf', '.txt': 'text/plain; charset=utf-8'
|
|
638
|
+
}
|
|
639
|
+
disposers.push(registerRoute({
|
|
640
|
+
kind: 'prefix',
|
|
641
|
+
path: ROUTE_BASE + '/template-file',
|
|
642
|
+
handler: (req, res) => {
|
|
643
|
+
const rest = decodeURIComponent(String(req.url || '').split('?')[0])
|
|
644
|
+
.slice((ROUTE_BASE + '/template-file/').length)
|
|
645
|
+
const parts = rest.split('/')
|
|
646
|
+
const id = String(parts[0] || '')
|
|
647
|
+
const rel = parts.slice(1).join('/')
|
|
648
|
+
if (!TEMPLATE_ID_RE.test(id) || !rel || rel.includes('..') || rel.includes(':')) { res.writeHead(404); res.end(); return }
|
|
649
|
+
if (!rel.split('/').every((seg) => TEMPLATE_FILE_SEG_RE.test(seg))) { res.writeHead(404); res.end(); return }
|
|
650
|
+
if (!TEMPLATE_FILE_EXT_RE.test(rel)) { res.writeHead(404); res.end(); return }
|
|
651
|
+
const roots = [path.join(USER_TPL_ROOT, id), path.join(PACKAGE_ROOT, 'templates', id)]
|
|
652
|
+
let target = null
|
|
653
|
+
for (const root of roots) {
|
|
654
|
+
const file = path.join(root, rel)
|
|
655
|
+
try { if (fs.statSync(file).isFile()) { target = file; break } } catch (err) { /* 下一处 */ }
|
|
656
|
+
}
|
|
657
|
+
if (!target) { res.writeHead(404); res.end(); return }
|
|
658
|
+
const ext = (rel.match(TEMPLATE_FILE_EXT_RE) || [''])[0].toLowerCase()
|
|
659
|
+
try {
|
|
660
|
+
let bytes = fs.readFileSync(target)
|
|
661
|
+
if (id === 'division-bigfish-codex' && rel === 'config.js') {
|
|
662
|
+
const cfg = readConfig()
|
|
663
|
+
const extra = []
|
|
664
|
+
if (cfg.bigfishUsername) extra.push('window.CONFIG.username=' + JSON.stringify(String(cfg.bigfishUsername)) + ';')
|
|
665
|
+
if (cfg.bigfishTitle) extra.push('window.CONFIG.title=' + JSON.stringify(String(cfg.bigfishTitle)) + ';')
|
|
666
|
+
if (cfg.bigfishDesc) extra.push('window.CONFIG.description=' + JSON.stringify(String(cfg.bigfishDesc)) + ';')
|
|
667
|
+
if (cfg.bigfishVoiceFx && cfg.bigfishVoiceFx !== 'none') extra.push('window.CONFIG.voiceFx=' + JSON.stringify(String(cfg.bigfishVoiceFx)) + ';')
|
|
668
|
+
const vv = Number(cfg.bigfishVoiceVolume); if (Number.isFinite(vv)) extra.push('window.CONFIG.voiceVolume=' + Math.max(0, Math.min(1, vv / 100)) + ';')
|
|
669
|
+
if (extra.length) bytes = Buffer.concat([bytes, Buffer.from('\n/* dsh-boot-suite overrides */\n' + extra.join('\n') + '\n', 'utf8')])
|
|
670
|
+
}
|
|
671
|
+
res.writeHead(200, { 'Content-Type': TEMPLATE_FILE_TYPES[ext] || 'application/octet-stream', 'Cache-Control': 'no-store' })
|
|
672
|
+
res.end(bytes)
|
|
673
|
+
} catch (err) { res.writeHead(500); res.end() }
|
|
674
|
+
}
|
|
675
|
+
}))
|
|
676
|
+
|
|
580
677
|
function readTemplate(id) {
|
|
581
678
|
if (!id || !TEMPLATE_ID_RE.test(String(id))) return null
|
|
582
679
|
try { if ((readConfig().hiddenTemplates || []).includes(String(id))) return null } catch (err) { /* 配置读不到就不隐藏 */ }
|
|
@@ -1194,6 +1291,102 @@ function apply(ctx) {
|
|
|
1194
1291
|
}
|
|
1195
1292
|
}))
|
|
1196
1293
|
|
|
1294
|
+
/* ── v2.14.0:用户自备背景音乐(BGM)────────────────────────────
|
|
1295
|
+
文件只存本机 ~/.dsh/startup-music,不上传任何服务器;设置页只保存文件名。 */
|
|
1296
|
+
const MUSIC_DIR = path.join(DSH_HOME, 'startup-music')
|
|
1297
|
+
const MUSIC_INDEX = path.join(MUSIC_DIR, 'index.json')
|
|
1298
|
+
const MUSIC_FILE_RE = /^music-[a-z0-9]{3,16}-[a-z0-9._-]{1,90}\.(mp3|wav|ogg|m4a|aac|flac)$/i
|
|
1299
|
+
const MUSIC_EXT_RE = /\.(mp3|wav|ogg|m4a|aac|flac)$/i
|
|
1300
|
+
const MUSIC_TYPES = { '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.m4a': 'audio/mp4', '.aac': 'audio/aac', '.flac': 'audio/flac' }
|
|
1301
|
+
function readMusicIndex() {
|
|
1302
|
+
try {
|
|
1303
|
+
const arr = JSON.parse(fs.readFileSync(MUSIC_INDEX, 'utf8'))
|
|
1304
|
+
return Array.isArray(arr) ? arr.filter((x) => x && typeof x.file === 'string' && MUSIC_FILE_RE.test(x.file)) : []
|
|
1305
|
+
} catch (e) { return [] }
|
|
1306
|
+
}
|
|
1307
|
+
function writeMusicIndex(arr) {
|
|
1308
|
+
try {
|
|
1309
|
+
fs.mkdirSync(MUSIC_DIR, { recursive: true })
|
|
1310
|
+
fs.writeFileSync(MUSIC_INDEX, JSON.stringify(arr, null, 2) + '\n', 'utf8')
|
|
1311
|
+
} catch (e) { /* 索引失败不影响文件本体 */ }
|
|
1312
|
+
}
|
|
1313
|
+
function sendAudioFile(res, file) {
|
|
1314
|
+
const full = path.join(MUSIC_DIR, file)
|
|
1315
|
+
const buf = fs.readFileSync(full)
|
|
1316
|
+
const ext = (file.match(MUSIC_EXT_RE) || ['.mp3'])[0].toLowerCase()
|
|
1317
|
+
res.writeHead(200, {
|
|
1318
|
+
'Content-Type': MUSIC_TYPES[ext] || 'application/octet-stream',
|
|
1319
|
+
'Content-Length': String(buf.length),
|
|
1320
|
+
'Cache-Control': 'no-store'
|
|
1321
|
+
})
|
|
1322
|
+
res.end(buf)
|
|
1323
|
+
}
|
|
1324
|
+
disposers.push(registerRoute({
|
|
1325
|
+
kind: 'exact',
|
|
1326
|
+
path: ROUTE_BASE + '/music/list',
|
|
1327
|
+
handler: async (req, res) => {
|
|
1328
|
+
if ((req.method || 'GET').toUpperCase() !== 'GET') { sendJson(res, 405, { ok: false }); return }
|
|
1329
|
+
try { sendJson(res, 200, { ok: true, files: readMusicIndex(), dir: MUSIC_DIR.replace(/\\/g, '/') }) }
|
|
1330
|
+
catch (err) { sendJson(res, 500, { ok: false, error: String((err && err.message) || err) }) }
|
|
1331
|
+
}
|
|
1332
|
+
}))
|
|
1333
|
+
disposers.push(registerRoute({
|
|
1334
|
+
kind: 'exact',
|
|
1335
|
+
path: ROUTE_BASE + '/music/file',
|
|
1336
|
+
handler: async (req, res) => {
|
|
1337
|
+
if ((req.method || 'GET').toUpperCase() !== 'GET') { sendJson(res, 405, { ok: false }); return }
|
|
1338
|
+
let u = null
|
|
1339
|
+
try { u = new URL(String(req.url || ''), 'http://localhost') } catch (eU) { sendJson(res, 400, { ok: false }); return }
|
|
1340
|
+
const name = String(u.searchParams.get('name') || '')
|
|
1341
|
+
if (!MUSIC_FILE_RE.test(name) || !fs.existsSync(path.join(MUSIC_DIR, name))) { sendJson(res, 404, { ok: false, error: 'music not found' }); return }
|
|
1342
|
+
try { sendAudioFile(res, name) } catch (err) { sendJson(res, 500, { ok: false, error: String((err && err.message) || err) }) }
|
|
1343
|
+
}
|
|
1344
|
+
}))
|
|
1345
|
+
disposers.push(registerRoute({
|
|
1346
|
+
kind: 'exact',
|
|
1347
|
+
path: ROUTE_BASE + '/music/upload',
|
|
1348
|
+
handler: async (req, res) => {
|
|
1349
|
+
if ((req.method || 'GET').toUpperCase() !== 'POST') { sendJson(res, 405, { ok: false }); return }
|
|
1350
|
+
let u = null
|
|
1351
|
+
try { u = new URL(String(req.url || ''), 'http://localhost') } catch (eU) { sendJson(res, 400, { ok: false }); return }
|
|
1352
|
+
const rawName = String(u.searchParams.get('name') || '').replace(/[\\/]/g, '').trim().slice(0, 160)
|
|
1353
|
+
const m = rawName.match(MUSIC_EXT_RE)
|
|
1354
|
+
if (!m) { sendJson(res, 400, { ok: false, error: '只支持 mp3 / wav / ogg / m4a / aac / flac' }); return }
|
|
1355
|
+
const bytes = await readRawBody(req, 100 * 1024 * 1024)
|
|
1356
|
+
if (!bytes || !bytes.length) { sendJson(res, 413, { ok: false, error: '文件为空或超过 100MB' }); return }
|
|
1357
|
+
const ext = m[0].toLowerCase()
|
|
1358
|
+
const stamp = Date.now().toString(36)
|
|
1359
|
+
const rand = Math.random().toString(36).slice(2, 8)
|
|
1360
|
+
const file = 'music-' + stamp + '-' + rand + ext
|
|
1361
|
+
try {
|
|
1362
|
+
fs.mkdirSync(MUSIC_DIR, { recursive: true })
|
|
1363
|
+
fs.writeFileSync(path.join(MUSIC_DIR, file), bytes)
|
|
1364
|
+
const list = readMusicIndex()
|
|
1365
|
+
list.unshift({ file, name: rawName.replace(MUSIC_EXT_RE, '').slice(0, 80) || file, size: bytes.length, added: Date.now() })
|
|
1366
|
+
writeMusicIndex(list.slice(0, 60))
|
|
1367
|
+
sendJson(res, 200, { ok: true, file, name: rawName.replace(MUSIC_EXT_RE, ''), size: bytes.length })
|
|
1368
|
+
} catch (err) { sendJson(res, 500, { ok: false, error: String((err && err.message) || err) }) }
|
|
1369
|
+
}
|
|
1370
|
+
}))
|
|
1371
|
+
disposers.push(registerRoute({
|
|
1372
|
+
kind: 'exact',
|
|
1373
|
+
path: ROUTE_BASE + '/music/delete',
|
|
1374
|
+
handler: async (req, res) => {
|
|
1375
|
+
if ((req.method || 'GET').toUpperCase() !== 'POST') { sendJson(res, 405, { ok: false }); return }
|
|
1376
|
+
const text = await readBody(req, 8 * 1024)
|
|
1377
|
+
let body = null
|
|
1378
|
+
try { body = JSON.parse(text) } catch (e) { body = null }
|
|
1379
|
+
const file = String((body && body.file) || '')
|
|
1380
|
+
if (!MUSIC_FILE_RE.test(file)) { sendJson(res, 400, { ok: false, error: 'bad file' }); return }
|
|
1381
|
+
try {
|
|
1382
|
+
const full = path.join(MUSIC_DIR, file)
|
|
1383
|
+
if (fs.existsSync(full)) fs.unlinkSync(full)
|
|
1384
|
+
writeMusicIndex(readMusicIndex().filter((x) => x.file !== file))
|
|
1385
|
+
sendJson(res, 200, { ok: true, file })
|
|
1386
|
+
} catch (err) { sendJson(res, 500, { ok: false, error: String((err && err.message) || err) }) }
|
|
1387
|
+
}
|
|
1388
|
+
}))
|
|
1389
|
+
|
|
1197
1390
|
disposers.push(registerRoute({
|
|
1198
1391
|
kind: 'exact',
|
|
1199
1392
|
path: ROUTE_BASE + '/templates/delete',
|
package/lib/host-wallpaper.js
CHANGED
|
@@ -695,19 +695,40 @@ export default {
|
|
|
695
695
|
}
|
|
696
696
|
}
|
|
697
697
|
|
|
698
|
+
/* v2.14.0:壁纸路由也接入信任栅栏(与 host-startup 同款;DSH connection 服务可拒绝未授权请求) */
|
|
699
|
+
function resolveWebServer(c) {
|
|
700
|
+
return (typeof c.get === 'function' ? c.get('webServer') : undefined) || c.webServer
|
|
701
|
+
}
|
|
702
|
+
function rejected(req, res) {
|
|
703
|
+
try {
|
|
704
|
+
const conn = (typeof ctx.get === 'function' ? ctx.get('connection') : undefined) || ctx.connection
|
|
705
|
+
if (!conn || typeof conn.requestRejection !== 'function') return false
|
|
706
|
+
const code = conn.requestRejection(req)
|
|
707
|
+
if (!code) return false
|
|
708
|
+
res.statusCode = code
|
|
709
|
+
res.end()
|
|
710
|
+
return true
|
|
711
|
+
} catch { return false }
|
|
712
|
+
}
|
|
713
|
+
function registerRoute(route) {
|
|
714
|
+
const ws = resolveWebServer(ctx)
|
|
715
|
+
const handler = route.handler
|
|
716
|
+
ws.register(Object.assign({}, route, { handler: (req, res) => (rejected(req, res) ? undefined : handler(req, res)) }))
|
|
717
|
+
}
|
|
718
|
+
|
|
698
719
|
/* v2.13.17:开机就把丢图目录建好;老用户/新装用户都不会再看到「?」 */
|
|
699
720
|
try { mkdirSync(wallpaperImageDir(), { recursive: true }) } catch { /* 忽略 */ }
|
|
700
|
-
ctx.effect(() =>
|
|
701
|
-
ctx.effect(() =>
|
|
702
|
-
ctx.effect(() =>
|
|
703
|
-
ctx.effect(() =>
|
|
704
|
-
ctx.effect(() =>
|
|
705
|
-
ctx.effect(() =>
|
|
706
|
-
ctx.effect(() =>
|
|
707
|
-
ctx.effect(() =>
|
|
708
|
-
ctx.effect(() =>
|
|
709
|
-
ctx.effect(() =>
|
|
710
|
-
ctx.effect(() =>
|
|
721
|
+
ctx.effect(() => registerRoute({ kind: 'exact', path: '/chat-wallpaper/config', handler: configHandler }))
|
|
722
|
+
ctx.effect(() => registerRoute({ kind: 'exact', path: '/chat-wallpaper/image', handler: imageHandler }))
|
|
723
|
+
ctx.effect(() => registerRoute({ kind: 'exact', path: '/chat-wallpaper/images', handler: imagesHandler }))
|
|
724
|
+
ctx.effect(() => registerRoute({ kind: 'exact', path: '/chat-wallpaper/set', handler: setHandler }))
|
|
725
|
+
ctx.effect(() => registerRoute({ kind: 'exact', path: '/chat-wallpaper/open-dir', handler: openDirHandler }))
|
|
726
|
+
ctx.effect(() => registerRoute({ kind: 'exact', path: '/chat-wallpaper/videos', handler: videosHandler }))
|
|
727
|
+
ctx.effect(() => registerRoute({ kind: 'exact', path: '/chat-wallpaper/video', handler: videoHandler }))
|
|
728
|
+
ctx.effect(() => registerRoute({ kind: 'exact', path: '/chat-wallpaper/video-preview', handler: videoPreviewHandler }))
|
|
729
|
+
ctx.effect(() => registerRoute({ kind: 'exact', path: '/chat-wallpaper/videos/delete', handler: videosDeleteHandler }))
|
|
730
|
+
ctx.effect(() => registerRoute({ kind: 'exact', path: '/chat-wallpaper/videos/roots', handler: videoRootsHandler }))
|
|
731
|
+
ctx.effect(() => registerRoute({ kind: 'exact', path: '/chat-wallpaper/status', handler: statusHandler }))
|
|
711
732
|
|
|
712
733
|
const cfg = readConfig()
|
|
713
734
|
const img = resolveImage(cfg)
|
package/lib/splash.js
CHANGED
|
@@ -90,6 +90,23 @@
|
|
|
90
90
|
voiceSource: 'asset', // asset=优先用真声素材 / tts=全部 TTS 合成
|
|
91
91
|
voiceLead: 240,
|
|
92
92
|
welcomeDelay: 550, // 终幕欢迎语相对 05→06 和声钟的延迟(ms)
|
|
93
|
+
/* v2.14.0 用户自备 BGM(与 lib/host-startup.js DEFAULTS 对齐) */
|
|
94
|
+
bgmEnabled: false,
|
|
95
|
+
bgmFile: '',
|
|
96
|
+
bgmFile2: '',
|
|
97
|
+
bgmVolume: 0.5,
|
|
98
|
+
bgmBpm: 100,
|
|
99
|
+
bgmBeats1: 16,
|
|
100
|
+
bgmBeats2: 32,
|
|
101
|
+
bgm1Start: 0,
|
|
102
|
+
bgm2Start: 0,
|
|
103
|
+
bgm1FadeIn: 1,
|
|
104
|
+
bgm1FadeOut: 1,
|
|
105
|
+
bgm2FadeIn: 1,
|
|
106
|
+
bgm2FadeOut: 2,
|
|
107
|
+
bgmCrossfade: 1200,
|
|
108
|
+
bgm2Loop: false,
|
|
109
|
+
bgmDuck: true,
|
|
93
110
|
identity: 'JOYCE MOORE', // ← 设置页可改的"身份名称"
|
|
94
111
|
identityId: '0087',
|
|
95
112
|
accessLevel: '3',
|
|
@@ -273,6 +290,10 @@
|
|
|
273
290
|
if (file && /^[a-z0-9][a-z0-9._-]{0,40}\.(mp3|wav|ogg|m4a|aac)$/i.test(file) && TEMPLATE && TEMPLATE.id) {
|
|
274
291
|
return '/dsh-startup/template-asset/' + encodeURIComponent(TEMPLATE.id) + '/' + file
|
|
275
292
|
}
|
|
293
|
+
/* v2.14.0:全局 line-N 已并入音色包,别再回落已删除的 lib/assets/line-N.mp3(会 404) */
|
|
294
|
+
var pack = (typeof C !== 'undefined' && C && C.voicePack) || 'f'
|
|
295
|
+
if (pack === 'builtin') pack = 'f'
|
|
296
|
+
if (/^line-[1-9]\d?$/.test(name)) return '/dsh-startup/voice/' + pack + '/' + name + '.mp3'
|
|
276
297
|
return '/dsh-startup/asset/' + name + '.mp3'
|
|
277
298
|
}
|
|
278
299
|
var MAX_STEPS = 24
|
|
@@ -1483,6 +1504,17 @@
|
|
|
1483
1504
|
preload: function () { loadAsset('sfx-tick'); loadAsset('sfx-ticks-run') },
|
|
1484
1505
|
ensureAsset: ensureAsset,
|
|
1485
1506
|
playAssetAndWait: playAssetAndWait,
|
|
1507
|
+
ctx: ac,
|
|
1508
|
+
decodeBuffer: function (ab) {
|
|
1509
|
+
var c = ac()
|
|
1510
|
+
return new Promise(function (resolve, reject) {
|
|
1511
|
+
if (!c) { reject(new Error('no audio context')); return }
|
|
1512
|
+
try {
|
|
1513
|
+
var p = c.decodeAudioData(ab, resolve, reject)
|
|
1514
|
+
if (p && typeof p.then === 'function') p.then(resolve, reject)
|
|
1515
|
+
} catch (e) { reject(e) }
|
|
1516
|
+
})
|
|
1517
|
+
},
|
|
1486
1518
|
/* 文字渐显 / 阶段推进 / 按钮确认 —— 优先用真素材 */
|
|
1487
1519
|
tick: function () { if (!playAsset('sfx-tick', 0.45)) tone(1760, 0.035, 'square', 0.022) },
|
|
1488
1520
|
confirm: function () { if (!playAsset('sfx-tick', 0.85)) { tone(660, 0.09, 'triangle', 0.07); tone(990, 0.17, 'triangle', 0.055, 0.085) } },
|
|
@@ -1655,7 +1687,9 @@
|
|
|
1655
1687
|
return audio.ensureAsset(useAsset).then(function (ok) {
|
|
1656
1688
|
if (!ok || finished) return 0
|
|
1657
1689
|
if (C.voiceCue !== false) audio.cue()
|
|
1690
|
+
if (isGlobalLine) bgm.duck(true)
|
|
1658
1691
|
return audio.playAssetAndWait(useAsset, 0.85, isGlobalLine ? fx : 'none').then(function (ms) {
|
|
1692
|
+
if (isGlobalLine) bgm.duck(false)
|
|
1659
1693
|
if (ms > 0) setMeter(true)
|
|
1660
1694
|
return ms
|
|
1661
1695
|
})
|
|
@@ -1798,11 +1832,243 @@
|
|
|
1798
1832
|
}
|
|
1799
1833
|
|
|
1800
1834
|
var runStarted = false
|
|
1835
|
+
/* ── v2.14.0 用户自备 BGM ───────────────────────────────────────
|
|
1836
|
+
只播放用户本机导入的音乐(宿主 /dsh-startup/music/file 提供)。
|
|
1837
|
+
三段:第一幕(loop) → 第二幕(可换曲) → 结束淡出;两段之间交叉叠化。
|
|
1838
|
+
任何异常都静默失败,绝不阻塞开机动画。 */
|
|
1839
|
+
var bgm = (function () {
|
|
1840
|
+
var ACTIVE = false
|
|
1841
|
+
var SECOND = false
|
|
1842
|
+
var cur = null /* { src, gain, name, which } */
|
|
1843
|
+
var baseVol = 0
|
|
1844
|
+
var buffered = {}
|
|
1845
|
+
var ducked = false
|
|
1846
|
+
|
|
1847
|
+
function num(v, d) { var n = Number(v); return isFinite(n) ? n : d }
|
|
1848
|
+
function clamp(v, lo, hi) { return Math.min(hi, Math.max(lo, v)) }
|
|
1849
|
+
function ctxNow() { var c = audio.ctx(); return c ? c.currentTime : 0 }
|
|
1850
|
+
|
|
1851
|
+
function segOf(which) {
|
|
1852
|
+
var bpm = clamp(num(C.bgmBpm, 100), 40, 220)
|
|
1853
|
+
var beats = which === 1 ? clamp(Math.round(num(C.bgmBeats1, 16)), 4, 128) : clamp(Math.round(num(C.bgmBeats2, 32)), 4, 128)
|
|
1854
|
+
return {
|
|
1855
|
+
start: Math.max(0, num(which === 1 ? C.bgm1Start : C.bgm2Start, 0)),
|
|
1856
|
+
dur: beats * 60 / bpm,
|
|
1857
|
+
fadeIn: clamp(num(which === 1 ? C.bgm1FadeIn : C.bgm2FadeIn, 1), 0, 10),
|
|
1858
|
+
fadeOut: clamp(num(which === 1 ? C.bgm1FadeOut : C.bgm2FadeOut, 1), 0, 10),
|
|
1859
|
+
loop: which === 2 ? (C.bgmLoop !== false && C.bgm2Loop === true) : (C.bgmLoop !== false)
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
function load(name) {
|
|
1864
|
+
if (!name) return Promise.resolve(null)
|
|
1865
|
+
if (buffered[name]) return Promise.resolve(buffered[name])
|
|
1866
|
+
return fetch('/dsh-startup/music/file?name=' + encodeURIComponent(name), { cache: 'no-store' })
|
|
1867
|
+
.then(function (r) { if (!r.ok) throw new Error('bgm http ' + r.status); return r.arrayBuffer() })
|
|
1868
|
+
.then(function (ab) { return audio.decodeBuffer(ab) })
|
|
1869
|
+
.then(function (buf) { if (buf) buffered[name] = buf; return buf })
|
|
1870
|
+
.catch(function () { return null })
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
function kill(fadeSec) {
|
|
1874
|
+
var old = cur
|
|
1875
|
+
cur = null
|
|
1876
|
+
if (!old) return
|
|
1877
|
+
try {
|
|
1878
|
+
var c = audio.ctx()
|
|
1879
|
+
var t = c ? c.currentTime : 0
|
|
1880
|
+
var g = old.gain.gain
|
|
1881
|
+
try {
|
|
1882
|
+
var v = g.value
|
|
1883
|
+
g.cancelScheduledValues(t)
|
|
1884
|
+
g.setValueAtTime(v, t)
|
|
1885
|
+
g.linearRampToValueAtTime(0, t + Math.max(0.05, fadeSec))
|
|
1886
|
+
} catch (e) {}
|
|
1887
|
+
var src = old.src
|
|
1888
|
+
var waitMs = Math.round(Math.max(0.05, fadeSec) * 1000) + 80
|
|
1889
|
+
setTimeout(function () { try { src.stop(); src.disconnect() } catch (e2) {} }, waitMs)
|
|
1890
|
+
setTimeout(function () { try { old.gain.disconnect() } catch (e3) {} }, waitMs + 50)
|
|
1891
|
+
} catch (e) {}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function startSeg(which, name) {
|
|
1895
|
+
return load(name).then(function (buf) {
|
|
1896
|
+
if (!ACTIVE || !buf) return
|
|
1897
|
+
var c = audio.ctx()
|
|
1898
|
+
if (!c) return
|
|
1899
|
+
var seg = segOf(which)
|
|
1900
|
+
var loop = seg.loop
|
|
1901
|
+
var dur = loop ? Math.min(buf.duration, Math.max(0.05, seg.dur)) : Math.max(0.05, buf.duration - seg.start)
|
|
1902
|
+
var src = c.createBufferSource()
|
|
1903
|
+
src.buffer = buf
|
|
1904
|
+
src.loop = loop
|
|
1905
|
+
var loopEnd = Math.min(buf.duration, seg.start + dur)
|
|
1906
|
+
if (seg.start >= buf.duration - 0.05) seg.start = 0
|
|
1907
|
+
src.loopStart = Math.max(0, Math.min(seg.start, buf.duration - 0.05))
|
|
1908
|
+
src.loopEnd = Math.max(src.loopStart + 0.05, loopEnd)
|
|
1909
|
+
var gain = c.createGain()
|
|
1910
|
+
gain.gain.value = 0
|
|
1911
|
+
src.connect(gain)
|
|
1912
|
+
gain.connect(c.destination)
|
|
1913
|
+
var t = c.currentTime
|
|
1914
|
+
var level = baseVol
|
|
1915
|
+
if (ducked && C.bgmDuck !== false) level = baseVol * 0.35
|
|
1916
|
+
var fi = Math.max(0.02, seg.fadeIn)
|
|
1917
|
+
try {
|
|
1918
|
+
gain.gain.setValueAtTime(0, t)
|
|
1919
|
+
gain.gain.linearRampToValueAtTime(level, t + fi)
|
|
1920
|
+
} catch (e) {}
|
|
1921
|
+
try { src.start(t, seg.start) } catch (e) { try { src.start() } catch (e2) { return } }
|
|
1922
|
+
cur = { src: src, gain: gain, name: name, which: which }
|
|
1923
|
+
})
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function begin() {
|
|
1927
|
+
if (C.bgmEnabled !== true) { diagRun('bgm off'); return }
|
|
1928
|
+
if (!C.bgmFile) { diagRun('bgm no file'); return }
|
|
1929
|
+
ACTIVE = true
|
|
1930
|
+
SECOND = false
|
|
1931
|
+
ducked = false
|
|
1932
|
+
baseVol = clamp(num(C.bgmVolume, 0.5), 0, 1)
|
|
1933
|
+
try { audio.ctx() } catch (e) {}
|
|
1934
|
+
diagRun('bgm start file=' + C.bgmFile)
|
|
1935
|
+
startSeg(1, String(C.bgmFile))
|
|
1936
|
+
/* 预载第二段,换曲时无空档 */
|
|
1937
|
+
var name2 = String(C.bgmFile2 || C.bgmFile)
|
|
1938
|
+
if (name2) load(name2)
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
return {
|
|
1942
|
+
start: function () { if (ACTIVE) return; begin() },
|
|
1943
|
+
/* v2.15.10:iframe 模板掉线彩蛋——弹出时截断,重连后重播 */
|
|
1944
|
+
restart: function () { if (ACTIVE) return; begin() },
|
|
1945
|
+
cut: function () { if (!ACTIVE) return; ACTIVE = false; diagRun('bgm cut'); kill(0.12) },
|
|
1946
|
+
toSecond: function () {
|
|
1947
|
+
if (!ACTIVE || SECOND) return
|
|
1948
|
+
SECOND = true
|
|
1949
|
+
var name = String(C.bgmFile2 || C.bgmFile || '')
|
|
1950
|
+
if (!name) return
|
|
1951
|
+
var cross = clamp(num(C.bgmCrossfade, 1200), 0, 5000) / 1000
|
|
1952
|
+
diagRun('bgm to second file=' + name)
|
|
1953
|
+
startSeg(2, name)
|
|
1954
|
+
kill(cross > 0 ? cross : 0.12)
|
|
1955
|
+
},
|
|
1956
|
+
stop: function () {
|
|
1957
|
+
if (!ACTIVE) return
|
|
1958
|
+
ACTIVE = false
|
|
1959
|
+
diagRun('bgm stop')
|
|
1960
|
+
var out = clamp(num(C.bgm2FadeOut, 2), 0, 10)
|
|
1961
|
+
kill(out > 0 ? out : 0.15)
|
|
1962
|
+
},
|
|
1963
|
+
duck: function (on) {
|
|
1964
|
+
if (!ACTIVE || !cur || C.bgmDuck === false) return
|
|
1965
|
+
ducked = !!on
|
|
1966
|
+
var c = audio.ctx()
|
|
1967
|
+
if (!c) return
|
|
1968
|
+
var t = c.currentTime
|
|
1969
|
+
var target = on ? baseVol * 0.35 : baseVol
|
|
1970
|
+
try {
|
|
1971
|
+
cur.gain.gain.cancelScheduledValues(t)
|
|
1972
|
+
cur.gain.gain.setTargetAtTime(target, t, on ? 0.12 : 0.25)
|
|
1973
|
+
} catch (e) {}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
})()
|
|
1977
|
+
|
|
1978
|
+
/* v2.15.0:iframe 启动模板(如 division-bigfish-codex)。
|
|
1979
|
+
整屏 iframe 自己演、自己判定彩蛋;宿主只负责挂载/收尾。
|
|
1980
|
+
- 完成:iframe postMessage {type:'dsh-startup-done'} 或 window 'dsh-startup-done' 事件
|
|
1981
|
+
- 跳过:postMessage skip / Esc / 设置页「立即预览」的跳过 API
|
|
1982
|
+
- 同源时顺手把作者编辑器侧栏藏掉,只留动画舞台。 */
|
|
1983
|
+
var iframeCleanup = null
|
|
1984
|
+
|
|
1985
|
+
function runIframeTemplate(tpl) {
|
|
1986
|
+
diagRun('iframe template ' + tpl.id)
|
|
1987
|
+
var entry = String(tpl.entry || 'index.html').replace(/^\/+/, '')
|
|
1988
|
+
var wrap = document.createElement('div')
|
|
1989
|
+
wrap.setAttribute('data-dsh-iframe-template', tpl.id)
|
|
1990
|
+
wrap.style.cssText = 'position:fixed;inset:0;z-index:2147483647;background:#000;overflow:hidden'
|
|
1991
|
+
var frame = document.createElement('iframe')
|
|
1992
|
+
frame.setAttribute('allow', 'autoplay; fullscreen')
|
|
1993
|
+
frame.setAttribute('allowfullscreen', 'true')
|
|
1994
|
+
frame.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:0;background:#000;display:block'
|
|
1995
|
+
frame.style.visibility = 'hidden'
|
|
1996
|
+
frame.src = '/dsh-startup/template-file/' + encodeURIComponent(tpl.id) + '/' + entry + (C.bigfishForceEgg ? '?egg=1' : '')
|
|
1997
|
+
wrap.appendChild(frame)
|
|
1998
|
+
document.body.appendChild(wrap)
|
|
1999
|
+
|
|
2000
|
+
var settle = null
|
|
2001
|
+
var done = new Promise(function (resolve) { settle = resolve })
|
|
2002
|
+
function finishOnce() { if (settle) { var fn2 = settle; settle = null; fn2() } }
|
|
2003
|
+
function onMessage(e) {
|
|
2004
|
+
var d = e && e.data
|
|
2005
|
+
if (d && typeof d === 'object' && (d.type === 'dsh-startup-done' || d.type === 'dsh-startup-skip')) finishOnce()
|
|
2006
|
+
/* v2.15.8:模板内配音开始时闪避 BGM */
|
|
2007
|
+
if (d && typeof d === 'object' && d.type === 'dsh-startup-voice') bgm.duck(d.on === true)
|
|
2008
|
+
if (d && typeof d === 'object' && d.type === 'dsh-startup-bgm') { if (d.on === false) bgm.cut(); else bgm.restart() }
|
|
2009
|
+
}
|
|
2010
|
+
function onKey(e) {
|
|
2011
|
+
if (e.key === 'Escape') { finishOnce(); return }
|
|
2012
|
+
/* v2.15.3:焦点被 DSH 输入框抢走后,父页面把空格转发进 iframe 切 tips */
|
|
2013
|
+
if (e.key === ' ' || e.key === 'Spacebar' || e.code === 'Space') {
|
|
2014
|
+
e.preventDefault()
|
|
2015
|
+
try { frame.contentWindow.postMessage({ type: 'dsh-tip-next' }, '*') } catch (eTip) { /* noop */ }
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
window.addEventListener('message', onMessage)
|
|
2019
|
+
window.addEventListener('dsh-startup-done', finishOnce)
|
|
2020
|
+
document.addEventListener('keydown', onKey, true)
|
|
2021
|
+
frame.addEventListener('load', function () {
|
|
2022
|
+
try {
|
|
2023
|
+
var doc = frame.contentDocument
|
|
2024
|
+
if (!doc || !doc.head) return
|
|
2025
|
+
var st = doc.createElement('style')
|
|
2026
|
+
st.textContent = 'html,body{overflow:hidden!important;background:#000!important}main{display:block!important}aside{display:none!important}#stage{width:100vw!important;height:100vh!important;display:grid!important;place-items:center!important}svg#scene{width:100vw!important;height:100vh!important;max-height:none!important}'
|
|
2027
|
+
doc.head.appendChild(st)
|
|
2028
|
+
} catch (err) { /* 跨域就跳过(同源正常情况下不会) */ }
|
|
2029
|
+
/* v2.15.2:把焦点交给 iframe,模板内才能收到空格切 tips;
|
|
2030
|
+
同时把 Esc 处理器注入 iframe(焦点进去后父页面的 keydown 收不到)。 */
|
|
2031
|
+
try {
|
|
2032
|
+
var ew = frame.contentWindow
|
|
2033
|
+
if (ew) {
|
|
2034
|
+
try { ew.focus() } catch (eFocus) { /* noop */ }
|
|
2035
|
+
doc.addEventListener('keydown', function (eKey) {
|
|
2036
|
+
if (eKey.key === 'Escape') {
|
|
2037
|
+
try { ew.parent.postMessage({ type: 'dsh-startup-skip' }, '*') } catch (e9) { /* noop */ }
|
|
2038
|
+
}
|
|
2039
|
+
})
|
|
2040
|
+
}
|
|
2041
|
+
} catch (err2) { /* noop */ }
|
|
2042
|
+
try { frame.style.visibility = 'visible' } catch (err) { /* noop */ }
|
|
2043
|
+
})
|
|
2044
|
+
setTimeout(function () { try { frame.style.visibility = 'visible' } catch (err) { /* noop */ } }, 2000)
|
|
2045
|
+
var safety = setTimeout(finishOnce, (Number(tpl.duration) > 0 ? Number(tpl.duration) * 1000 : 45000) + 90000)
|
|
2046
|
+
iframeCleanup = function () {
|
|
2047
|
+
clearTimeout(safety)
|
|
2048
|
+
window.removeEventListener('message', onMessage)
|
|
2049
|
+
window.removeEventListener('dsh-startup-done', finishOnce)
|
|
2050
|
+
document.removeEventListener('keydown', onKey, true)
|
|
2051
|
+
try { if (wrap.parentNode) wrap.parentNode.removeChild(wrap) } catch (err) { /* noop */ }
|
|
2052
|
+
finishOnce()
|
|
2053
|
+
}
|
|
2054
|
+
return done.then(function () {
|
|
2055
|
+
/* v2.15.11:模板不再自己做白闪;结尾由插件退场转场接管。
|
|
2056
|
+
这里不提前卸 iframe / 停 BGM,交给被转场盖住的 finish() 统一清理。 */
|
|
2057
|
+
return transOut(transType('Out'), finish)
|
|
2058
|
+
})
|
|
2059
|
+
}
|
|
2060
|
+
|
|
1801
2061
|
function run() {
|
|
1802
2062
|
if (runStarted) { diagRun('run skipped (duplicate entry)'); return Promise.resolve() }
|
|
1803
2063
|
runStarted = true
|
|
2064
|
+
if (TEMPLATE && TEMPLATE.engine === 'iframe' && TEMPLATE.id) {
|
|
2065
|
+
/* v2.15.8:iframe 模板也支持用户自备 BGM(父页面播,模板自己演) */
|
|
2066
|
+
try { bgm.start() } catch (eBgmIframe) { /* 静默失败,绝不阻塞启动 */ }
|
|
2067
|
+
return runIframeTemplate(TEMPLATE)
|
|
2068
|
+
}
|
|
1804
2069
|
diagRun('run enter timeline=' + (TIMELINE ? 'v2' : 'v1') + ' tpl=' + ((TEMPLATE && TEMPLATE.id) || '?') + ' tpl2=' + ((TEMPLATE2 && TEMPLATE2.id) || 'none'))
|
|
1805
2070
|
audio.bedStart() /* 本地补丁:开机即"开通道",底噪一路铺到揭幕 */
|
|
2071
|
+
bgm.start()
|
|
1806
2072
|
/* v2 时间线模板:不演档案开场,直接跑图层时序 */
|
|
1807
2073
|
if (TIMELINE) {
|
|
1808
2074
|
/* v2.12.0:第一幕入场转场(非默认类型才挂覆盖层,且非阻塞 —— 第一幕照跑) */
|
|
@@ -2880,6 +3146,8 @@
|
|
|
2880
3146
|
if (!TEMPLATE2 || !Array.isArray(TEMPLATE2.layers) || !TEMPLATE2.layers.length) {
|
|
2881
3147
|
return Promise.resolve()
|
|
2882
3148
|
}
|
|
3149
|
+
/* v2.15.9:没有第二幕就不切第二段音乐(原来提前调用,导致「不播放第二幕」也会换曲) */
|
|
3150
|
+
bgm.toSecond()
|
|
2883
3151
|
/* v2.12.0:衔接/退场选择在切换点现场读(设置页「立即预览」会换掉 window.__DSH_STARTUP__) */
|
|
2884
3152
|
var joinType = transType('Join')
|
|
2885
3153
|
var joinOverlay = transActive(joinType)
|
|
@@ -3578,6 +3846,8 @@
|
|
|
3578
3846
|
}
|
|
3579
3847
|
|
|
3580
3848
|
function finish() {
|
|
3849
|
+
bgm.stop()
|
|
3850
|
+
if (iframeCleanup) { try { var cleanIframe = iframeCleanup; iframeCleanup = null; cleanIframe() } catch (eIframe) { /* noop */ } }
|
|
3581
3851
|
if (finished) return
|
|
3582
3852
|
diagRun('finish videos=' + videoCount())
|
|
3583
3853
|
finished = true
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-boot-suite",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.15.15",
|
|
4
|
+
"description": "两幕启动动画(默认档案暖白 + 鲸鱼娘扫描睁眼,2K/4K 可切),支持导入视频/图片、三段转场、11→6 套配音音色与 5 种声音效果、聊天背景/循环视频、内置 iframe 启动模板(全景大肥鱼-含彩蛋版)与更新前自动回滚。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|