dsh-boot-suite 2.16.2 → 2.16.8
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 +40 -0
- package/lib/assets/builtin-background-snowgirl.png +0 -0
- package/lib/client.js +240 -46
- package/lib/host-startup.js +4 -1
- package/lib/host-wallpaper.js +40 -18
- package/lib/splash.js +27 -5
- package/package.json +1 -1
- package/templates/division-bigfish-codex/assets/egg-bg.png +0 -0
- package/test/smoke-client.mjs +28 -19
- package/test/smoke-package.mjs +32 -1
- package/test/smoke-wallpaper-client.mjs +1 -1
- package/test/smoke-wallpaper-host.mjs +9 -6
- package/test/smoke-wallpaper-panel.mjs +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
## [2.16.8] — 2026-11-04
|
|
2
|
+
|
|
3
|
+
- 聊天背景新增第二张内置背景「黑发雪夜战术少女」(`lib/assets/builtin-background-snowgirl.png`):设置 → 聊天背景 → 背景图里会多一张可点选,默认仍用原默认背景,不覆盖用户已选。
|
|
4
|
+
- 聊天背景设置面板:内置背景条目支持多张(`id` / `builtinId`),当前选中项按 id 精确高亮;「当前状态」显示对应内置名称。
|
|
5
|
+
- 掉线彩蛋底图沿用上一版替换后的图片。
|
|
6
|
+
- 版本 2.16.8,构建号 cw-74。
|
|
7
|
+
|
|
8
|
+
## [2.16.7] — 2026-11-04
|
|
9
|
+
|
|
10
|
+
- 新增「视频原声」开关:第一幕、第二幕各一个,可分别决定是否播放视频自带的配音/音乐(默认静音,保持原有行为)。适合把配好音的导入动画当第二幕使用;打开后视频原声与背景音乐同时响,建议只开一个。
|
|
11
|
+
- 未静音视频若被浏览器自动播放策略拦下,会自动退回静音再播,保证画面不会卡住。
|
|
12
|
+
- 掉线彩蛋底图换成「黑发雪夜战术少女」。
|
|
13
|
+
- 版本 2.16.7,构建号 cw-73。
|
|
14
|
+
|
|
15
|
+
## [2.16.6] — 2026-11-04
|
|
16
|
+
|
|
17
|
+
- 修复「第一幕是 iframe 模板(全景大肥鱼-含彩蛋版)时,第二幕永远不播」:`lib/splash.js` 的 iframe 分支此前在模板结束后直接收尾,绕过了 `playSecondTemplate()`;现在 iframe 收尾前先接管第二幕,再走退场转场。
|
|
18
|
+
- 无第二幕时行为不变(直接退场);第二幕的衔接/退场转场仍按设置生效。
|
|
19
|
+
- 版本 2.16.6,构建号 cw-72。
|
|
20
|
+
|
|
21
|
+
## [2.16.5] — 2026-11-04
|
|
22
|
+
|
|
23
|
+
- 下拉菜单加不透明底:皮肤把 `--dsw-alias-bg-layer-2` 定义成半透明,菜单会透出背后的说明文字。现在改用近不透明的 `--dsw-alias-bg-overlay` 双层叠加(≈99%,跟随浅/深主题)+ 轻微背景模糊,选项清晰可读。
|
|
24
|
+
- 悬停底色、选中项颜色补了 fallback,皮肤未定义对应 token 时也能正常高亮。
|
|
25
|
+
- 版本 2.16.5,构建号 cw-71。
|
|
26
|
+
|
|
27
|
+
## [2.16.4] — 2026-11-04
|
|
28
|
+
|
|
29
|
+
- 修复 v2.16.3 的自绘下拉:10 条新样式少了开头的 `.`,实际没有生效——触发按钮的框消失、菜单没有外框、每个选项露出浏览器默认白边。
|
|
30
|
+
- 现在触发按钮恢复原生 select 的边框/底色/内边距,下拉菜单整块是一个带边框和阴影的框(随皮肤 3D 面板倾斜),选项是紧凑的纯文字行(无白框),排版与原生下拉列表一致。
|
|
31
|
+
- 版本 2.16.4,构建号 cw-70。
|
|
32
|
+
|
|
33
|
+
## [2.16.3] — 2026-11-04
|
|
34
|
+
|
|
35
|
+
- 设置页所有下拉(启动动画、聊天背景、配音音色、转场等)改为自绘菜单,不再用系统原生 `<select>`:原生弹层无法被 CSS 3D 变换作用,所以之前不跟封锁橙皮肤的倾斜面板。
|
|
36
|
+
- 自绘菜单渲染在设置面板内部,自动继承面板的 `perspective(1000px) rotateY(-6.5deg) rotateX(1.6deg)`,展开时与面板同一个 3D 平面;默认皮肤和其他皮肤没有这段变换,菜单自然保持水平(无需额外检测)。
|
|
37
|
+
- 下拉空间不足时自动向上展开;支持 Esc 关闭、点击外部关闭、滚动关闭,键盘可 Tab/Enter 操作。
|
|
38
|
+
- 聊天背景的「图片适配 / 背景类型 / 播放模式 / 色彩模式」四个下拉同样改为自绘。
|
|
39
|
+
- 版本 2.16.3,构建号 cw-69。
|
|
40
|
+
|
|
1
41
|
## [2.16.2] — 2026-11-03
|
|
2
42
|
|
|
3
43
|
- **安装器代理加固**:开着代理安装时,pnpm 访问 npm registry 可能长时间卡在 [2/3](用户实测等很久无响应,关代理马上恢复)。现在会先检测并提示(5 秒后自动继续),同时把 pnpm 的 fetch 超时收紧到 30 秒、失败后自动用国内镜像重试一次。
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -60,7 +60,17 @@ window.__ModuleLoader__.load({
|
|
|
60
60
|
'.__ss_err{font-size:12px;color:var(--dsw-alias-state-error-primary,#f85149)}',
|
|
61
61
|
'.__ss_swatch{width:38px;height:26px;padding:0;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;cursor:pointer}',
|
|
62
62
|
'.__ss_preview{margin-top:2px;border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:10px 12px;display:flex;align-items:center;justify-content:space-between;gap:12px}',
|
|
63
|
-
'.__ss_previewText{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.5}'
|
|
63
|
+
'.__ss_previewText{font-size:12px;color:var(--dsw-alias-label-tertiary);line-height:1.5}',
|
|
64
|
+
'.__ss_selectWrap{position:relative;display:inline-block;min-width:180px;max-width:100%}',
|
|
65
|
+
'.__ss_select{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;cursor:pointer;text-align:left}',
|
|
66
|
+
'.__ss_select.is-open{border-color:var(--dsw-alias-state-business-primary)}',
|
|
67
|
+
'.__ss_selectLabel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
68
|
+
'.__ss_selectCaret{flex:none;opacity:.7;font-size:11px}',
|
|
69
|
+
'.__ss_selectMenu{position:absolute;left:0;right:0;top:calc(100% + 4px);z-index:40;display:flex;flex-direction:column;gap:0;padding:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background-color:#1b1d22;background-image:linear-gradient(var(--dsw-alias-bg-overlay, rgba(22,23,27,.95)), var(--dsw-alias-bg-overlay, rgba(22,23,27,.95))),linear-gradient(var(--dsw-alias-bg-overlay, rgba(22,23,27,.95)), var(--dsw-alias-bg-overlay, rgba(22,23,27,.95)));backdrop-filter:blur(8px);box-shadow:0 10px 28px rgba(0,0,0,.45);max-height:280px;overflow:auto;box-sizing:border-box}',
|
|
70
|
+
'.__ss_selectMenu.is-up{top:auto;bottom:calc(100% + 4px)}',
|
|
71
|
+
'.__ss_selectItem{display:block;width:100%;text-align:left;border:none;background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font:inherit;font-size:13px;cursor:pointer}',
|
|
72
|
+
'.__ss_selectItem:hover{background:var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.12))}',
|
|
73
|
+
'.__ss_selectItem.is-on{color:var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary, #e6762b));font-weight:600}'
|
|
64
74
|
].join('')
|
|
65
75
|
var tagId = 'dsh-startup-screen/main.css'
|
|
66
76
|
if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="' + tagId + '"]') === null) {
|
|
@@ -93,13 +103,109 @@ window.__ModuleLoader__.load({
|
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
function Select(props) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
var openState = react.useState(false)
|
|
107
|
+
var open = openState[0]
|
|
108
|
+
var setOpen = openState[1]
|
|
109
|
+
var upState = react.useState(false)
|
|
110
|
+
var openUp = upState[0]
|
|
111
|
+
var setOpenUp = upState[1]
|
|
112
|
+
var wrapRef = react.useRef(null)
|
|
113
|
+
var btnRef = react.useRef(null)
|
|
114
|
+
var opts = props.options || []
|
|
115
|
+
var cur = opts.find(function (o) { return String(o[0]) === String(props.value) }) || opts[0] || ['', '']
|
|
116
|
+
var label = cur[1]
|
|
117
|
+
|
|
118
|
+
function measure() {
|
|
119
|
+
var el = btnRef.current
|
|
120
|
+
if (!el) return
|
|
121
|
+
var r = el.getBoundingClientRect()
|
|
122
|
+
var topLimit = 8
|
|
123
|
+
var bottomLimit = (window.innerHeight || 800) - 8
|
|
124
|
+
var node = el.parentNode
|
|
125
|
+
while (node && node.nodeType === 1 && node !== document.body) {
|
|
126
|
+
var st = window.getComputedStyle(node)
|
|
127
|
+
if (/(auto|scroll)/.test(st.overflowY) && node.clientHeight > 0) {
|
|
128
|
+
var nr = node.getBoundingClientRect()
|
|
129
|
+
topLimit = Math.max(topLimit, nr.top + 2)
|
|
130
|
+
bottomLimit = Math.min(bottomLimit, nr.bottom - 2)
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
node = node.parentNode
|
|
134
|
+
}
|
|
135
|
+
var below = bottomLimit - r.bottom
|
|
136
|
+
var above = r.top - topLimit
|
|
137
|
+
var useUp = below < 280 && above > below
|
|
138
|
+
setOpenUp(useUp)
|
|
139
|
+
var maxH = Math.max(120, Math.min(280, (useUp ? above : below) - 8))
|
|
140
|
+
if (wrapRef.current) {
|
|
141
|
+
wrapRef.current.style.setProperty('--ss-menu-max', maxH + 'px')
|
|
142
|
+
var menu = wrapRef.current.querySelector('.__ss_selectMenu')
|
|
143
|
+
if (menu) menu.style.maxHeight = maxH + 'px'
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
react.useEffect(function () {
|
|
148
|
+
if (!open) return
|
|
149
|
+
measure()
|
|
150
|
+
function onDocDown(e) {
|
|
151
|
+
if (!wrapRef.current) return
|
|
152
|
+
if (!wrapRef.current.contains(e.target)) setOpen(false)
|
|
153
|
+
}
|
|
154
|
+
function onKey(e) {
|
|
155
|
+
if (e.key === 'Escape') {
|
|
156
|
+
setOpen(false)
|
|
157
|
+
try { btnRef.current.focus() } catch (err) {}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function onScroll(e) {
|
|
161
|
+
if (!wrapRef.current) return
|
|
162
|
+
if (!wrapRef.current.contains(e.target)) setOpen(false)
|
|
163
|
+
}
|
|
164
|
+
document.addEventListener('mousedown', onDocDown, true)
|
|
165
|
+
document.addEventListener('keydown', onKey, true)
|
|
166
|
+
window.addEventListener('scroll', onScroll, true)
|
|
167
|
+
return function () {
|
|
168
|
+
document.removeEventListener('mousedown', onDocDown, true)
|
|
169
|
+
document.removeEventListener('keydown', onKey, true)
|
|
170
|
+
window.removeEventListener('scroll', onScroll, true)
|
|
171
|
+
}
|
|
172
|
+
}, [open])
|
|
173
|
+
|
|
174
|
+
return h('div', { className: '__ss_selectWrap' + (open ? ' is-open' : ''), ref: wrapRef },
|
|
175
|
+
h('button', {
|
|
176
|
+
className: '__ss_select' + (open ? ' is-open' : ''),
|
|
177
|
+
type: 'button',
|
|
178
|
+
ref: btnRef,
|
|
179
|
+
'aria-haspopup': 'listbox',
|
|
180
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
181
|
+
onClick: function () { setOpen(!open) }
|
|
182
|
+
},
|
|
183
|
+
h('span', { className: '__ss_selectLabel' }, label),
|
|
184
|
+
h('span', { className: '__ss_selectCaret', 'aria-hidden': 'true' }, 'v')
|
|
185
|
+
),
|
|
186
|
+
open ? h('div', {
|
|
187
|
+
className: '__ss_selectMenu' + (openUp ? ' is-up' : ''),
|
|
188
|
+
role: 'listbox',
|
|
189
|
+
style: { maxHeight: 'var(--ss-menu-max, 280px)' }
|
|
190
|
+
}, opts.map(function (o) {
|
|
191
|
+
var on = String(o[0]) === String(props.value)
|
|
192
|
+
return h('button', {
|
|
193
|
+
key: o[0],
|
|
194
|
+
type: 'button',
|
|
195
|
+
role: 'option',
|
|
196
|
+
'aria-selected': on ? 'true' : 'false',
|
|
197
|
+
className: '__ss_selectItem' + (on ? ' is-on' : ''),
|
|
198
|
+
onClick: function () {
|
|
199
|
+
setOpen(false)
|
|
200
|
+
if (on) return
|
|
201
|
+
props.onChange(o[0])
|
|
202
|
+
}
|
|
203
|
+
}, o[1])
|
|
204
|
+
})) : null
|
|
205
|
+
)
|
|
101
206
|
}
|
|
102
207
|
|
|
208
|
+
|
|
103
209
|
function Check(props) {
|
|
104
210
|
return h('label', { className: '__ss_check' },
|
|
105
211
|
h('input', {
|
|
@@ -256,7 +362,8 @@ window.__ModuleLoader__.load({
|
|
|
256
362
|
colorMode: 'color',
|
|
257
363
|
bgmEnabled: false, bgmFile: '', bgmFile2: '', bgmVolume: 0.5, bgmBpm: 100, bgmBeats1: 16, bgmBeats2: 32,
|
|
258
364
|
bgm1Start: 0, bgm2Start: 0, bgm1FadeIn: 1, bgm1FadeOut: 1, bgm2FadeIn: 1, bgm2FadeOut: 2,
|
|
259
|
-
bgmCrossfade: 1200, bgmLoop: true, bgm2Loop: false, bgmDuck: true
|
|
365
|
+
bgmCrossfade: 1200, bgmLoop: true, bgm2Loop: false, bgmDuck: true,
|
|
366
|
+
act1VideoSound: false, act2VideoSound: false
|
|
260
367
|
}
|
|
261
368
|
function num(v, d) {
|
|
262
369
|
var n = Number(v)
|
|
@@ -269,7 +376,7 @@ window.__ModuleLoader__.load({
|
|
|
269
376
|
—— 桌面端走浏览器半侧自举时没有 1,就靠这一条
|
|
270
377
|
3) 下面这个常量:两份都没拿到时的兜底,别让面板显示 undefined
|
|
271
378
|
版本号本身只在 lib/index.js 里读一次,这里不重复维护。 */
|
|
272
|
-
var FALLBACK_VERSION = '2.16.
|
|
379
|
+
var FALLBACK_VERSION = '2.16.8'
|
|
273
380
|
/* 手动选包成功后置 true:让还在后台跑的“浏览器下载监听”不要再弹第二次更新器 */
|
|
274
381
|
var __manualUpdateApplied = false
|
|
275
382
|
function readInjectedVersion() {
|
|
@@ -1160,6 +1267,12 @@ window.__ModuleLoader__.load({
|
|
|
1160
1267
|
}, '删除该动画')
|
|
1161
1268
|
)
|
|
1162
1269
|
),
|
|
1270
|
+
h(Field, {
|
|
1271
|
+
label: '第一幕视频原声',
|
|
1272
|
+
hint: '第一幕使用视频/导入动画时,是否播放视频自带的声音;默认静音。'
|
|
1273
|
+
},
|
|
1274
|
+
h(Check, { checked: draft.act1VideoSound === true, onChange: (v) => set('act1VideoSound', v), label: '播放第一幕视频原声' })
|
|
1275
|
+
),
|
|
1163
1276
|
h(Field, {
|
|
1164
1277
|
label: '启动动画第二幕',
|
|
1165
1278
|
hint: '第一幕播完接着播这一段;选「不播放」就第一幕结束直接进聊天。'
|
|
@@ -1188,6 +1301,12 @@ window.__ModuleLoader__.load({
|
|
|
1188
1301
|
}, '删除该动画')
|
|
1189
1302
|
)
|
|
1190
1303
|
),
|
|
1304
|
+
h(Field, {
|
|
1305
|
+
label: '第二幕视频原声',
|
|
1306
|
+
hint: '第二幕视频里自带配音/音乐时打开;默认静音。打开后会和背景音乐同时响。'
|
|
1307
|
+
},
|
|
1308
|
+
h(Check, { checked: draft.act2VideoSound === true, onChange: (v) => set('act2VideoSound', v), label: '播放第二幕视频原声' })
|
|
1309
|
+
),
|
|
1191
1310
|
h('div', { className: '__ss_row' },
|
|
1192
1311
|
h('button', {
|
|
1193
1312
|
className: '__ss_btn',
|
|
@@ -2022,7 +2141,7 @@ window.__ModuleLoader__.load({
|
|
|
2022
2141
|
/* 每次改客户端就把这个号 +1。
|
|
2023
2142
|
有了它在控制台一眼就能判断"浏览器里跑的是不是最新那份" ——
|
|
2024
2143
|
排查问题时,分不清是代码问题还是旧包,会白猜一轮。 */
|
|
2025
|
-
var BUILD = 'cw-
|
|
2144
|
+
var BUILD = 'cw-74'
|
|
2026
2145
|
|
|
2027
2146
|
function log() {
|
|
2028
2147
|
try {
|
|
@@ -2979,6 +3098,107 @@ window.__ModuleLoader__.load({
|
|
|
2979
3098
|
与「启动动画」并列(order 26 → 27)。数据走宿主自带的 HTTP 接口(/chat-wallpaper/*)。 */
|
|
2980
3099
|
var cwReact = require('react')
|
|
2981
3100
|
var cwH = cwReact.createElement
|
|
3101
|
+
/* v2.16.3:自绘下拉(跟随设置面板的 3D 平面;原生 <select> 的系统弹层无法被 CSS 变换) */
|
|
3102
|
+
function CwSelect(props) {
|
|
3103
|
+
var openState = cwReact.useState(false)
|
|
3104
|
+
var open = openState[0]
|
|
3105
|
+
var setOpen = openState[1]
|
|
3106
|
+
var upState = cwReact.useState(false)
|
|
3107
|
+
var openUp = upState[0]
|
|
3108
|
+
var setOpenUp = upState[1]
|
|
3109
|
+
var wrapRef = cwReact.useRef(null)
|
|
3110
|
+
var btnRef = cwReact.useRef(null)
|
|
3111
|
+
var opts = props.options || []
|
|
3112
|
+
var cur = opts.find(function (o) { return String(o[0]) === String(props.value) }) || opts[0] || ['', '']
|
|
3113
|
+
|
|
3114
|
+
function measure() {
|
|
3115
|
+
var el = btnRef.current
|
|
3116
|
+
if (!el) return
|
|
3117
|
+
var r = el.getBoundingClientRect()
|
|
3118
|
+
var topLimit = 8
|
|
3119
|
+
var bottomLimit = (window.innerHeight || 800) - 8
|
|
3120
|
+
var node = el.parentNode
|
|
3121
|
+
while (node && node.nodeType === 1 && node !== document.body) {
|
|
3122
|
+
var st = window.getComputedStyle(node)
|
|
3123
|
+
if (/(auto|scroll)/.test(st.overflowY) && node.clientHeight > 0) {
|
|
3124
|
+
var nr = node.getBoundingClientRect()
|
|
3125
|
+
topLimit = Math.max(topLimit, nr.top + 2)
|
|
3126
|
+
bottomLimit = Math.min(bottomLimit, nr.bottom - 2)
|
|
3127
|
+
break
|
|
3128
|
+
}
|
|
3129
|
+
node = node.parentNode
|
|
3130
|
+
}
|
|
3131
|
+
var below = bottomLimit - r.bottom
|
|
3132
|
+
var above = r.top - topLimit
|
|
3133
|
+
var useUp = below < 280 && above > below
|
|
3134
|
+
setOpenUp(useUp)
|
|
3135
|
+
var maxH = Math.max(120, Math.min(280, (useUp ? above : below) - 8))
|
|
3136
|
+
if (wrapRef.current) {
|
|
3137
|
+
var menu = wrapRef.current.querySelector('.__ss_selectMenu')
|
|
3138
|
+
if (menu) menu.style.maxHeight = maxH + 'px'
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
cwReact.useEffect(function () {
|
|
3143
|
+
if (!open) return
|
|
3144
|
+
measure()
|
|
3145
|
+
function onDocDown(e) {
|
|
3146
|
+
if (!wrapRef.current) return
|
|
3147
|
+
if (!wrapRef.current.contains(e.target)) setOpen(false)
|
|
3148
|
+
}
|
|
3149
|
+
function onKey(e) {
|
|
3150
|
+
if (e.key === 'Escape') {
|
|
3151
|
+
setOpen(false)
|
|
3152
|
+
try { btnRef.current.focus() } catch (err) {}
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
function onScroll(e) {
|
|
3156
|
+
if (!wrapRef.current) return
|
|
3157
|
+
if (!wrapRef.current.contains(e.target)) setOpen(false)
|
|
3158
|
+
}
|
|
3159
|
+
document.addEventListener('mousedown', onDocDown, true)
|
|
3160
|
+
document.addEventListener('keydown', onKey, true)
|
|
3161
|
+
window.addEventListener('scroll', onScroll, true)
|
|
3162
|
+
return function () {
|
|
3163
|
+
document.removeEventListener('mousedown', onDocDown, true)
|
|
3164
|
+
document.removeEventListener('keydown', onKey, true)
|
|
3165
|
+
window.removeEventListener('scroll', onScroll, true)
|
|
3166
|
+
}
|
|
3167
|
+
}, [open])
|
|
3168
|
+
|
|
3169
|
+
return cwH('div', { className: '__ss_selectWrap' + (open ? ' is-open' : ''), ref: wrapRef },
|
|
3170
|
+
cwH('button', {
|
|
3171
|
+
className: '__ss_select' + (open ? ' is-open' : ''),
|
|
3172
|
+
type: 'button',
|
|
3173
|
+
ref: btnRef,
|
|
3174
|
+
disabled: !!props.disabled,
|
|
3175
|
+
'aria-haspopup': 'listbox',
|
|
3176
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
3177
|
+
onClick: function () { if (!props.disabled) setOpen(!open) }
|
|
3178
|
+
},
|
|
3179
|
+
cwH('span', { className: '__ss_selectLabel' }, cur[1]),
|
|
3180
|
+
cwH('span', { className: '__ss_selectCaret', 'aria-hidden': 'true' }, 'v')
|
|
3181
|
+
),
|
|
3182
|
+
open ? cwH('div', { className: '__ss_selectMenu' + (openUp ? ' is-up' : ''), role: 'listbox' },
|
|
3183
|
+
opts.map(function (o) {
|
|
3184
|
+
var on = String(o[0]) === String(props.value)
|
|
3185
|
+
return cwH('button', {
|
|
3186
|
+
key: o[0],
|
|
3187
|
+
type: 'button',
|
|
3188
|
+
role: 'option',
|
|
3189
|
+
'aria-selected': on ? 'true' : 'false',
|
|
3190
|
+
className: '__ss_selectItem' + (on ? ' is-on' : ''),
|
|
3191
|
+
onClick: function () {
|
|
3192
|
+
setOpen(false)
|
|
3193
|
+
if (on) return
|
|
3194
|
+
props.onChange(o[0])
|
|
3195
|
+
}
|
|
3196
|
+
}, o[1])
|
|
3197
|
+
})
|
|
3198
|
+
) : null
|
|
3199
|
+
)
|
|
3200
|
+
}
|
|
3201
|
+
|
|
2982
3202
|
|
|
2983
3203
|
/** 把 rgba(...) 里的 alpha 换掉,保留 RGB。 */
|
|
2984
3204
|
function withAlpha(color, a) {
|
|
@@ -3009,6 +3229,8 @@ window.__ModuleLoader__.load({
|
|
|
3009
3229
|
images: images.images || [],
|
|
3010
3230
|
current: images.current || null,
|
|
3011
3231
|
imageSource: images.imageSource || '',
|
|
3232
|
+
builtinId: images.builtinId || '',
|
|
3233
|
+
builtinName: images.builtinName || '',
|
|
3012
3234
|
dropDir: images.dropDir || '',
|
|
3013
3235
|
dropDirExists: !!images.dropDirExists,
|
|
3014
3236
|
veil: images.veil || { light: 0.8, dark: 0.5 },
|
|
@@ -3111,14 +3333,14 @@ window.__ModuleLoader__.load({
|
|
|
3111
3333
|
|
|
3112
3334
|
var thumb = function (im) {
|
|
3113
3335
|
var selected = im.builtin
|
|
3114
|
-
? data.imageSource === 'builtin'
|
|
3336
|
+
? (im.builtinId ? data.builtinId === im.builtinId : (im.selected != null ? im.selected : data.imageSource === 'builtin'))
|
|
3115
3337
|
: (data.current && data.current.split(/[\\/]/).pop() === im.name)
|
|
3116
3338
|
return cwH('button', {
|
|
3117
3339
|
key: im.name,
|
|
3118
3340
|
type: 'button',
|
|
3119
|
-
title: im.builtin ? '
|
|
3341
|
+
title: im.builtin ? ('内置背景:' + im.name + '(随插件安装;删不掉)') : (im.name + ' · ' + im.sizeKB + ' KB · ' + im.mtime),
|
|
3120
3342
|
disabled: busy,
|
|
3121
|
-
onClick: function () { apply({ image: im.builtin ? BUILTIN_IMAGE_ID : im.name }, true) },
|
|
3343
|
+
onClick: function () { apply({ image: im.builtin ? (im.builtinId || BUILTIN_IMAGE_ID) : im.name }, true) },
|
|
3122
3344
|
style: {
|
|
3123
3345
|
position: 'relative', padding: 0, border: '2px solid ' + (selected ? 'var(--dsw-alias-state-business-primary,#3964fe)' : 'var(--dsw-alias-border-l2)'),
|
|
3124
3346
|
borderRadius: 8, background: 'var(--dsw-alias-bg-layer-3)', cursor: 'pointer', overflow: 'hidden', width: 132, height: 76
|
|
@@ -3136,7 +3358,7 @@ window.__ModuleLoader__.load({
|
|
|
3136
3358
|
cwH('div', { className: '__ss_groupTitle' }, '当前状态'),
|
|
3137
3359
|
cwH('div', { className: '__ss_hint' },
|
|
3138
3360
|
'背景图:' + (data.imageSource === 'builtin'
|
|
3139
|
-
? '默认背景(内置)'
|
|
3361
|
+
? (data.builtinName || '默认背景(内置)')
|
|
3140
3362
|
: (data.image ? data.image.split(/[\\/]/).pop() : '(没有背景图,使用 DSH 默认底色)')),
|
|
3141
3363
|
cwH('br'),
|
|
3142
3364
|
'循环视频:' + (data.video && data.video.items.length ? ('已扫描 ' + data.video.items.length + ' 段' + (data.video.bgMode === 'video' && data.video.currentId ? ',正在播放' : ',未启用')) : '(视频目录里还没有视频)'),
|
|
@@ -3160,20 +3382,13 @@ window.__ModuleLoader__.load({
|
|
|
3160
3382
|
|
|
3161
3383
|
cwH('div', { className: '__ss_group' },
|
|
3162
3384
|
cwH('div', { className: '__ss_groupTitle' }, '背景图'),
|
|
3163
|
-
cwH('div', { className: '__ss_hint' }, '
|
|
3385
|
+
cwH('div', { className: '__ss_hint' }, '点一下立刻换。前两张是插件自带的内置背景(默认 / 黑发雪夜战术少女),开箱就有;把图丢进上面的目录也会出现在这里,按修改时间取最新那张。'),
|
|
3164
3386
|
data.images.length
|
|
3165
3387
|
? cwH('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } }, data.images.map(thumb))
|
|
3166
3388
|
: cwH('div', { className: '__ss_hint' }, '目录里还没有图。点「打开图片文件夹」,把 jpg / png / webp 丢进去,再点刷新。只扫描这一层,子文件夹里的不算(支持 jpg/png/webp/gif/avif/bmp)。'),
|
|
3167
3389
|
cwH('div', { className: '__ss_row', style: { marginTop: 8 } },
|
|
3168
3390
|
cwH('span', { className: '__ss_status' }, '图片适配'),
|
|
3169
|
-
cwH('
|
|
3170
|
-
className: '__ss_select', disabled: busy,
|
|
3171
|
-
value: data.imageFit,
|
|
3172
|
-
onChange: function (ev) { apply({ imageFit: ev.target.value }, true) }
|
|
3173
|
-
},
|
|
3174
|
-
cwH('option', { value: 'cover' }, '铺满窗口(裁切多余部分)'),
|
|
3175
|
-
cwH('option', { value: 'contain' }, '完整显示(不裁切,留底色边)')
|
|
3176
|
-
)
|
|
3391
|
+
cwH(CwSelect, { value: data.imageFit, disabled: busy, options: [['cover', '铺满窗口(裁切多余部分)'], ['contain', '完整显示(不裁切,留底色边)']], onChange: function (v) { apply({ imageFit: v }, true) } })
|
|
3177
3392
|
),
|
|
3178
3393
|
cwH('div', { className: '__ss_row' },
|
|
3179
3394
|
cwH('button', { className: '__ss_btn', type: 'button', disabled: busy, onClick: function () { apply({ image: NO_IMAGE_ID }, true) } }, '不用背景图')
|
|
@@ -3187,34 +3402,13 @@ window.__ModuleLoader__.load({
|
|
|
3187
3402
|
),
|
|
3188
3403
|
cwH('div', { className: '__ss_row', style: { marginTop: 8 } },
|
|
3189
3404
|
cwH('span', { className: '__ss_status' }, '背景类型'),
|
|
3190
|
-
cwH('
|
|
3191
|
-
className: '__ss_select', disabled: busy,
|
|
3192
|
-
value: data.video.bgMode,
|
|
3193
|
-
onChange: function (ev) { apply({ bgMode: ev.target.value }, true) }
|
|
3194
|
-
},
|
|
3195
|
-
cwH('option', { value: 'image' }, '静态底图'),
|
|
3196
|
-
cwH('option', { value: 'video' }, '循环视频')
|
|
3197
|
-
),
|
|
3405
|
+
cwH(CwSelect, { value: data.video.bgMode, disabled: busy, options: [['image', '静态底图'], ['video', '循环视频']], onChange: function (v) { apply({ bgMode: v }, true) } }),
|
|
3198
3406
|
cwH('span', { className: '__ss_status', style: { marginLeft: 16 } }, '播放模式'),
|
|
3199
|
-
cwH('
|
|
3200
|
-
className: '__ss_select', disabled: busy,
|
|
3201
|
-
value: data.video.videoMode,
|
|
3202
|
-
onChange: function (ev) { apply({ videoMode: ev.target.value }, true) }
|
|
3203
|
-
},
|
|
3204
|
-
cwH('option', { value: 'single' }, '手动选择'),
|
|
3205
|
-
cwH('option', { value: 'shuffle' }, '随机轮换')
|
|
3206
|
-
)
|
|
3407
|
+
cwH(CwSelect, { value: data.video.videoMode, disabled: busy, options: [['single', '手动选择'], ['shuffle', '随机轮换']], onChange: function (v) { apply({ videoMode: v }, true) } })
|
|
3207
3408
|
),
|
|
3208
3409
|
cwH('div', { className: '__ss_row', style: { marginTop: 8 } },
|
|
3209
3410
|
cwH('span', { className: '__ss_status' }, '色彩模式'),
|
|
3210
|
-
cwH('
|
|
3211
|
-
className: '__ss_select', disabled: busy,
|
|
3212
|
-
value: data.colorMode,
|
|
3213
|
-
onChange: function (ev) { apply({ colorMode: ev.target.value }, true) }
|
|
3214
|
-
},
|
|
3215
|
-
cwH('option', { value: 'color' }, '彩色(默认)'),
|
|
3216
|
-
cwH('option', { value: 'mono' }, '黑白(原版素材,不染色)')
|
|
3217
|
-
)
|
|
3411
|
+
cwH(CwSelect, { value: data.colorMode, disabled: busy, options: [['color', '彩色(默认)'], ['mono', '黑白(原版素材,不染色)']], onChange: function (v) { apply({ colorMode: v }, true) } })
|
|
3218
3412
|
),
|
|
3219
3413
|
cwH('div', { className: '__ss_hint' }, '黑白模式会关掉主题色纱、保留原画面颜色,适合本身就是黑白的动态壁纸;彩色模式不做任何调色。'),
|
|
3220
3414
|
data.video.videoMode === 'shuffle'
|
package/lib/host-startup.js
CHANGED
|
@@ -115,6 +115,9 @@ const DEFAULTS = {
|
|
|
115
115
|
glitchB: '#2971b8',
|
|
116
116
|
template: 'archive-warm', // 第一幕默认:档案暖白
|
|
117
117
|
act2: 'whale-girl-scan', // 第二幕默认:鲸鱼娘扫描睁眼(2K);另有 whale-girl-scan-4k;'' = 不播
|
|
118
|
+
/* v2.16.7:视频原声(第一幕 / 第二幕分开控制;默认静音) */
|
|
119
|
+
act1VideoSound: false,
|
|
120
|
+
act2VideoSound: false,
|
|
118
121
|
/* v2.12.0 三段转场:第一幕入场 / 两幕衔接 / 第二幕退场。默认全部交叉叠化。 */
|
|
119
122
|
transitionIn: 'dissolve',
|
|
120
123
|
transitionJoin: 'dissolve',
|
|
@@ -166,7 +169,7 @@ const ENUMS = {
|
|
|
166
169
|
transitionOut: TRANSITION_TYPES,
|
|
167
170
|
transitionDir: TRANSITION_DIRS
|
|
168
171
|
}
|
|
169
|
-
const BOOL_KEYS = ['enabled', 'requireInteraction', 'allowSkip', 'sound', 'bedNoise', 'voice', 'voiceCue', 'bgmEnabled', 'bgmLoop', 'bgm2Loop', 'bgmDuck']
|
|
172
|
+
const BOOL_KEYS = ['enabled', 'requireInteraction', 'allowSkip', 'sound', 'bedNoise', 'voice', 'voiceCue', 'bgmEnabled', 'bgmLoop', 'bgm2Loop', 'bgmDuck', 'act1VideoSound', 'act2VideoSound']
|
|
170
173
|
/** 数值字段的合法区间(越界即夹取)。 */
|
|
171
174
|
const RANGES = {
|
|
172
175
|
welcomeDelay: [0, 3000],
|
package/lib/host-wallpaper.js
CHANGED
|
@@ -21,13 +21,26 @@ const HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
|
21
21
|
const CONFIG_FILE = join(HOME, 'chat-wallpaper.json')
|
|
22
22
|
|
|
23
23
|
/* v2.13.1:插件自带一张默认背景图(随安装包上传,新装用户开箱就有底图)。
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
v2.16.8:新增第二张内置背景(可选,默认仍是第一张);两张都随包分发、删不掉。
|
|
25
|
+
哨兵值写进 config.image:
|
|
26
|
+
__default__ = 明确选内置默认图(用户自己的图不参与自动挑选)
|
|
27
|
+
__snowgirl__ = 明确选内置第二张「黑发雪夜战术少女」
|
|
28
|
+
__none__ = 明确不要背景图(否则空字符串在"丢图目录里有图"时会自动挑最新那张) */
|
|
29
|
+
const ASSETS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'assets')
|
|
30
|
+
const BUILTIN_IMAGES = [
|
|
31
|
+
{ id: '__default__', name: '默认背景(内置)', file: join(ASSETS_DIR, 'default-background.jpg') },
|
|
32
|
+
{ id: '__snowgirl__', name: '黑发雪夜战术少女(内置)', file: join(ASSETS_DIR, 'builtin-background-snowgirl.png') },
|
|
33
|
+
]
|
|
34
|
+
const BUILTIN_IMAGE = BUILTIN_IMAGES[0].file
|
|
35
|
+
const BUILTIN_IMAGE_ID = BUILTIN_IMAGES[0].id
|
|
29
36
|
const NO_IMAGE_ID = '__none__'
|
|
30
37
|
|
|
38
|
+
/** 按哨兵 id 找内置图 */
|
|
39
|
+
function builtinById(id) {
|
|
40
|
+
if (!id) return null
|
|
41
|
+
return BUILTIN_IMAGES.find((b) => b.id === id) || null
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
/** 默认配置。字段与客户端半边一一对应。 */
|
|
32
45
|
const DEFAULTS = {
|
|
33
46
|
/* 聊天底图:往 ~/.dsh/chat-wallpaper/ 里丢张图就自动换背景。
|
|
@@ -158,13 +171,17 @@ function isImageFile(p) {
|
|
|
158
171
|
}
|
|
159
172
|
function resolveImage(cfg) {
|
|
160
173
|
if (cfg.image === NO_IMAGE_ID) return { file: null, source: 'none' }
|
|
161
|
-
|
|
162
|
-
|
|
174
|
+
const picked = builtinById(cfg.image)
|
|
175
|
+
if (picked) {
|
|
176
|
+
return isImageFile(picked.file)
|
|
177
|
+
? { file: picked.file, source: 'builtin', builtinId: picked.id, builtinName: picked.name }
|
|
178
|
+
: { file: null, source: 'none' }
|
|
163
179
|
}
|
|
164
180
|
if (cfg.image && isImageFile(cfg.image)) return { file: cfg.image, source: 'config' }
|
|
165
181
|
const list = listImages()
|
|
166
182
|
if (list.length) return { file: list[0].file, source: 'your-library' }
|
|
167
|
-
|
|
183
|
+
const fallback = BUILTIN_IMAGES[0]
|
|
184
|
+
if (isImageFile(fallback.file)) return { file: fallback.file, source: 'builtin', builtinId: fallback.id, builtinName: fallback.name }
|
|
168
185
|
return { file: null, source: 'none' }
|
|
169
186
|
}
|
|
170
187
|
|
|
@@ -374,12 +391,13 @@ export default {
|
|
|
374
391
|
let file = null
|
|
375
392
|
if (want) {
|
|
376
393
|
const wanted = String(want)
|
|
377
|
-
|
|
378
|
-
|
|
394
|
+
const picked = builtinById(wanted)
|
|
395
|
+
if (picked) {
|
|
396
|
+
if (isImageFile(picked.file)) file = picked.file
|
|
379
397
|
} else {
|
|
380
398
|
const cand = join(wallpaperImageDir(), basename(wanted))
|
|
381
399
|
if (isImageFile(cand)) file = cand
|
|
382
|
-
/* 兼容旧 URL
|
|
400
|
+
/* 兼容旧 URL:只有用户目录没有同名文件时才回落到内置默认图 */
|
|
383
401
|
else if (wanted === 'default-background.jpg' && isImageFile(BUILTIN_IMAGE)) file = BUILTIN_IMAGE
|
|
384
402
|
}
|
|
385
403
|
} else {
|
|
@@ -416,18 +434,22 @@ export default {
|
|
|
416
434
|
dropDirExists: existsSync(dir),
|
|
417
435
|
current: cur.file ? cur.file.replace(/\\/g, '/') : null,
|
|
418
436
|
imageSource: cur.source,
|
|
437
|
+
builtinId: cur.builtinId || '',
|
|
438
|
+
builtinName: cur.builtinName || '',
|
|
419
439
|
imageConfig: cfg.image || '',
|
|
420
440
|
banner: cfg.banner || '',
|
|
421
441
|
bannerOpacity: cfg.bannerOpacity == null ? 0.12 : cfg.bannerOpacity,
|
|
422
442
|
images: [
|
|
423
|
-
...(existsSync(
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
443
|
+
...BUILTIN_IMAGES.filter((b) => existsSync(b.file)).map((b) => ({
|
|
444
|
+
id: b.id,
|
|
445
|
+
builtinId: b.id,
|
|
446
|
+
name: b.name,
|
|
447
|
+
url: '/chat-wallpaper/image?name=' + encodeURIComponent(b.id),
|
|
448
|
+
sizeKB: Math.round(statSync(b.file).size / 1024),
|
|
427
449
|
mtime: '',
|
|
428
450
|
builtin: true,
|
|
429
|
-
selected: cur.source === 'builtin',
|
|
430
|
-
}
|
|
451
|
+
selected: cur.source === 'builtin' && cur.builtinId === b.id,
|
|
452
|
+
})),
|
|
431
453
|
...listImages().map((c) => {
|
|
432
454
|
const name = c.file.split(/[\\/]/).pop()
|
|
433
455
|
return {
|
|
@@ -503,7 +525,7 @@ export default {
|
|
|
503
525
|
if (k === 'image') {
|
|
504
526
|
/* 只允许:空字符串(自动/内置默认)、两个哨兵、丢图目录里的文件名。
|
|
505
527
|
不接受任意绝对路径(避免成为本地任意文件读取原语)。 */
|
|
506
|
-
if (v === '' || v === NO_IMAGE_ID || v
|
|
528
|
+
if (v === '' || v === NO_IMAGE_ID || builtinById(String(v))) {
|
|
507
529
|
cfg.image = String(v || '')
|
|
508
530
|
applied.image = cfg.image
|
|
509
531
|
continue
|
package/lib/splash.js
CHANGED
|
@@ -489,6 +489,8 @@
|
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
var C = cfg()
|
|
492
|
+
/* v2.16.7:当前正在构建哪一幕的时间线(1=第一幕,2=第二幕),用于视频原声开关 */
|
|
493
|
+
var TIMELINE_ACT = 1
|
|
492
494
|
|
|
493
495
|
/* 模板的配色。两条规矩:
|
|
494
496
|
1) 用户手动挑过的颜色优先 —— 只有配置还是默认值时才让模板改;
|
|
@@ -1982,7 +1984,7 @@
|
|
|
1982
1984
|
- 同源时顺手把作者编辑器侧栏藏掉,只留动画舞台。 */
|
|
1983
1985
|
var iframeCleanup = null
|
|
1984
1986
|
|
|
1985
|
-
function runIframeTemplate(tpl) {
|
|
1987
|
+
function runIframeTemplate(tpl, afterIframe) {
|
|
1986
1988
|
diagRun('iframe template ' + tpl.id)
|
|
1987
1989
|
var entry = String(tpl.entry || 'index.html').replace(/^\/+/, '')
|
|
1988
1990
|
var wrap = document.createElement('div')
|
|
@@ -2052,6 +2054,9 @@
|
|
|
2052
2054
|
finishOnce()
|
|
2053
2055
|
}
|
|
2054
2056
|
return done.then(function () {
|
|
2057
|
+
/* v2.16.6:第一幕是 iframe 模板时,收尾前先把控制权交给调用方(接第二幕);
|
|
2058
|
+
没有 afterIframe 时保持原行为——直接走退场转场。 */
|
|
2059
|
+
if (typeof afterIframe === 'function') return afterIframe()
|
|
2055
2060
|
/* v2.15.11:模板不再自己做白闪;结尾由插件退场转场接管。
|
|
2056
2061
|
这里不提前卸 iframe / 停 BGM,交给被转场盖住的 finish() 统一清理。 */
|
|
2057
2062
|
return transOut(transType('Out'), finish)
|
|
@@ -2064,7 +2069,15 @@
|
|
|
2064
2069
|
if (TEMPLATE && TEMPLATE.engine === 'iframe' && TEMPLATE.id) {
|
|
2065
2070
|
/* v2.15.8:iframe 模板也支持用户自备 BGM(父页面播,模板自己演) */
|
|
2066
2071
|
try { bgm.start() } catch (eBgmIframe) { /* 静默失败,绝不阻塞启动 */ }
|
|
2067
|
-
|
|
2072
|
+
/* v2.16.6:iframe 第一幕结束后接第二幕(此前直接 finish,配了第二幕也永远不播) */
|
|
2073
|
+
return runIframeTemplate(TEMPLATE, function () {
|
|
2074
|
+
if (iframeCleanup) { try { iframeCleanup() } catch (eCleanIframe) { /* noop */ } iframeCleanup = null }
|
|
2075
|
+
return playSecondTemplate()
|
|
2076
|
+
.then(function () { return secondTransitionDone ? finish() : transOut(transType('Out'), finish) })
|
|
2077
|
+
}).catch(function (errIframe) {
|
|
2078
|
+
try { console.warn('[dsh-startup-screen] iframe+第二幕异常,直接收尾:', errIframe) } catch (e2) {}
|
|
2079
|
+
return finish()
|
|
2080
|
+
})
|
|
2068
2081
|
}
|
|
2069
2082
|
diagRun('run enter timeline=' + (TIMELINE ? 'v2' : 'v1') + ' tpl=' + ((TEMPLATE && TEMPLATE.id) || '?') + ' tpl2=' + ((TEMPLATE2 && TEMPLATE2.id) || 'none'))
|
|
2070
2083
|
audio.bedStart() /* 本地补丁:开机即"开通道",底噪一路铺到揭幕 */
|
|
@@ -2453,8 +2466,10 @@
|
|
|
2453
2466
|
var vnode = document.createElement('video')
|
|
2454
2467
|
vnode.className = 'dsu-tl-layer dsu-tl-video'
|
|
2455
2468
|
vnode.setAttribute('data-layer', String(L.id || ('video-' + i)))
|
|
2456
|
-
|
|
2457
|
-
|
|
2469
|
+
/* v2.16.7:视频原声开关——第一幕用 act1VideoSound,第二幕用 act2VideoSound;默认静音 */
|
|
2470
|
+
var vSoundOn = TIMELINE_ACT === 2 ? (C.act2VideoSound === true) : (C.act1VideoSound === true)
|
|
2471
|
+
vnode.muted = !vSoundOn
|
|
2472
|
+
vnode.defaultMuted = !vSoundOn
|
|
2458
2473
|
vnode.autoplay = true
|
|
2459
2474
|
vnode.playsInline = true
|
|
2460
2475
|
vnode.preload = 'auto'
|
|
@@ -2495,7 +2510,13 @@
|
|
|
2495
2510
|
diagRun('video append ' + vfile + ' vids=' + videoCount())
|
|
2496
2511
|
try {
|
|
2497
2512
|
var vp0 = vnode.play()
|
|
2498
|
-
if (vp0 && vp0.catch) vp0.catch(function () {
|
|
2513
|
+
if (vp0 && vp0.catch) vp0.catch(function (eAudio) {
|
|
2514
|
+
if (!vSoundOn) return
|
|
2515
|
+
/* v2.16.7:未静音自动播放可能被浏览器策略拦下——退回静音再播,保证画面不卡 */
|
|
2516
|
+
try { vnode.muted = true; vnode.defaultMuted = true } catch (eMute) {}
|
|
2517
|
+
try { var vpM = vnode.play(); if (vpM && vpM.catch) vpM.catch(function () {}) } catch (eMute2) {}
|
|
2518
|
+
diagRun('video audio blocked, fallback muted ' + vfile)
|
|
2519
|
+
})
|
|
2499
2520
|
} catch (ePlay) { /* autoplay blocked */ }
|
|
2500
2521
|
if (vnode.requestVideoFrameCallback) {
|
|
2501
2522
|
var vFrames = 0, vT0 = 0
|
|
@@ -3184,6 +3205,7 @@
|
|
|
3184
3205
|
/* 第二幕分支是否已经自己处理过「第二幕→桌面」退场(v2 主链路据此决定要不要再包 transOut) */
|
|
3185
3206
|
var secondTransitionDone = false
|
|
3186
3207
|
function playSecondTemplate() {
|
|
3208
|
+
TIMELINE_ACT = 2
|
|
3187
3209
|
if (secondStarted) { diagRun('second skipped (duplicate)'); return Promise.resolve() }
|
|
3188
3210
|
secondStarted = true
|
|
3189
3211
|
diagRun('second enter tlSwap=' + !!tlSwap)
|
package/package.json
CHANGED
|
Binary file
|
package/test/smoke-client.mjs
CHANGED
|
@@ -34,6 +34,13 @@ const React = {
|
|
|
34
34
|
const i = hookIndex++
|
|
35
35
|
if (!ranEffects.has(i)) { ranEffects.add(i); fn() }
|
|
36
36
|
},
|
|
37
|
+
useRef(init) {
|
|
38
|
+
const i = hookIndex++
|
|
39
|
+
if (hooks[i] === undefined) hooks[i] = { current: init === undefined ? null : init }
|
|
40
|
+
return hooks[i]
|
|
41
|
+
},
|
|
42
|
+
useMemo(fn) { return fn() },
|
|
43
|
+
useCallback(fn) { return fn },
|
|
37
44
|
useStateReset() {},
|
|
38
45
|
}
|
|
39
46
|
function resetHooks() { hookIndex = 0 }
|
|
@@ -180,6 +187,8 @@ function walk(node, out = []) {
|
|
|
180
187
|
if (typeof node !== 'object') return out /* 文本节点本身不收集 */
|
|
181
188
|
if (Array.isArray(node)) { node.forEach((n) => walk(n, out)); return out }
|
|
182
189
|
if (node.type) {
|
|
190
|
+
/* 自绘下拉 Select 有 hooks,不能直接调用;把它当成一个「下拉节点」收集 */
|
|
191
|
+
if (typeof node.type === 'function' && node.type.name === 'Select') { out.push(node); return out }
|
|
183
192
|
/* 函数组件要展开(Field / Check / TextInput… 都不用 hooks,直接调用即可) */
|
|
184
193
|
if (typeof node.type === 'function') return walk(node.type(node.props || {}), out)
|
|
185
194
|
out.push(node)
|
|
@@ -255,8 +264,8 @@ for (const label of ['第一幕入场', '第一幕 → 第二幕', '第二幕
|
|
|
255
264
|
|
|
256
265
|
/* 三段转场的下拉:8 种类型 × 3 个;方向下拉:4 个 */
|
|
257
266
|
const selectOf = (values) => walk(tree).filter((n) =>
|
|
258
|
-
n.type === '
|
|
259
|
-
values.every((v) => n.props.
|
|
267
|
+
typeof n.type === 'function' && Array.isArray(n.props.options) &&
|
|
268
|
+
values.every((v) => n.props.options.some((o) => o && o[0] === v)))
|
|
260
269
|
const TRANSITION_TYPES = ['dissolve', 'flash', 'glitch', 'scanline', 'slide', 'iris', 'black', 'none']
|
|
261
270
|
const TRANSITION_DIRS = ['left', 'right', 'up', 'down']
|
|
262
271
|
const typeSelects = selectOf(TRANSITION_TYPES)
|
|
@@ -283,10 +292,10 @@ check('文件选择器 accept 白名单含 mp4/webm/gif/png/jpg/jpeg/webp',
|
|
|
283
292
|
fileInput && fileInput.accept)
|
|
284
293
|
|
|
285
294
|
/* 选择转场 → 保存 patch 里必须带上(沿用现有的 vm 渲染 + 保存路径) */
|
|
286
|
-
typeSelects[0].props.onChange(
|
|
287
|
-
typeSelects[1].props.onChange(
|
|
288
|
-
typeSelects[2].props.onChange(
|
|
289
|
-
dirSelects[0].props.onChange(
|
|
295
|
+
typeSelects[0].props.onChange('glitch')
|
|
296
|
+
typeSelects[1].props.onChange('flash')
|
|
297
|
+
typeSelects[2].props.onChange('scanline')
|
|
298
|
+
dirSelects[0].props.onChange('up')
|
|
290
299
|
msRange.props.onChange({ target: { value: '900' } })
|
|
291
300
|
tree = renderPanel()
|
|
292
301
|
const typeSelects2 = selectOf(TRANSITION_TYPES)
|
|
@@ -322,17 +331,17 @@ console.log('\n[5c] 删除该动画:本地删文件 / 内置隐藏可恢复(
|
|
|
322
331
|
const groupTitles = () => walk(tree).filter((n) => n.type === 'div' && n.props && n.props.className === '__ss_groupTitle').map((n) => n.props.children)
|
|
323
332
|
const postList = (prefix) => requests.filter((r) => r.init && r.init.method === 'POST' && String(r.url).includes(prefix))
|
|
324
333
|
const restoreBtn = () => find(tree, (n) => n.type === 'button' && /^恢复已隐藏的内置模板(\d+)$/.test(String(n.props.children || '')))
|
|
325
|
-
const hasOption = (sel, id) => !!sel && Array.isArray(sel.props.
|
|
326
|
-
sel.props.
|
|
334
|
+
const hasOption = (sel, id) => !!sel && Array.isArray(sel.props.options) &&
|
|
335
|
+
sel.props.options.some((o) => o && o[0] === id)
|
|
327
336
|
/* 第一幕/第二幕两个模板下拉:都含内置 archive-warm,且都有一项「内置/不播放」说明 */
|
|
328
337
|
const templateSelects = () => walk(tree).filter((n) =>
|
|
329
|
-
n.type === '
|
|
330
|
-
n.props.
|
|
331
|
-
n.props.
|
|
338
|
+
typeof n.type === 'function' && Array.isArray(n.props.options) &&
|
|
339
|
+
n.props.options.some((o) => o && (o[0] === 'archive-warm' || o[0] === 'whale-girl-scan')) &&
|
|
340
|
+
n.props.options.some((o) => o && /内置|不播放/.test(String(o[1]))))
|
|
332
341
|
/* archive-warm 被隐藏后上面的定位会失效:改用「有内置/不播放说明」来找两个下拉 */
|
|
333
342
|
const templateSelectsAny = () => walk(tree).filter((n) =>
|
|
334
|
-
n.type === '
|
|
335
|
-
n.props.
|
|
343
|
+
typeof n.type === 'function' && Array.isArray(n.props.options) &&
|
|
344
|
+
n.props.options.some((o) => o && /内置|不播放/.test(String(o[1]))))
|
|
336
345
|
|
|
337
346
|
/* ── 组顺序:自定义动画 → 模板 → 转场(保留) ── */
|
|
338
347
|
const titles = groupTitles()
|
|
@@ -380,7 +389,7 @@ console.log('\n[5c] 删除该动画:本地删文件 / 内置隐藏可恢复(
|
|
|
380
389
|
/* ── 选内置模板 → 按钮也启用(v2.12.4 不再只对本地生效) ── */
|
|
381
390
|
let tplSel = templateSelects()
|
|
382
391
|
check('找到第一幕 / 第二幕两个模板下拉', tplSel.length === 2, '实际 ' + tplSel.length)
|
|
383
|
-
tplSel[0].props.onChange(
|
|
392
|
+
tplSel[0].props.onChange('archive-warm')
|
|
384
393
|
tree = renderPanel()
|
|
385
394
|
rows = tplRows()
|
|
386
395
|
check('★ 第一幕选内置 archive-warm → 第一幕按钮 enabled(title=隐藏内置模板(可恢复)),第二幕未选仍置灰',
|
|
@@ -389,7 +398,7 @@ console.log('\n[5c] 删除该动画:本地删文件 / 内置隐藏可恢复(
|
|
|
389
398
|
rowValue(rows[1]) === '' && rowBtn(rows[1]).props.disabled === true,
|
|
390
399
|
JSON.stringify(rows.map((r) => ({ v: rowValue(r), disabled: rowBtn(r).props.disabled, title: rowBtn(r).props.title }))))
|
|
391
400
|
tplSel = templateSelects()
|
|
392
|
-
tplSel[1].props.onChange(
|
|
401
|
+
tplSel[1].props.onChange('whale-girl-scan')
|
|
393
402
|
tree = renderPanel()
|
|
394
403
|
rows = tplRows()
|
|
395
404
|
check('★ 第二幕也选内置 whale-girl-scan → 两个按钮都 enabled',
|
|
@@ -450,7 +459,7 @@ console.log('\n[5c] 删除该动画:本地删文件 / 内置隐藏可恢复(
|
|
|
450
459
|
|
|
451
460
|
/* ── 本地模板删除路径保持不变(删文件、不可恢复、不进 hidden) ── */
|
|
452
461
|
tplSel = templateSelects()
|
|
453
|
-
tplSel[0].props.onChange(
|
|
462
|
+
tplSel[0].props.onChange('custom-local-a')
|
|
454
463
|
tree = renderPanel()
|
|
455
464
|
rows = tplRows()
|
|
456
465
|
check('第一幕选本地模板 → 按钮 enabled(title=删除这个本地动画)',
|
|
@@ -502,10 +511,10 @@ check('立即预览先重新拉配置', requests.length > beforePreview && reque
|
|
|
502
511
|
check('立即预览调用 __DSH_STARTUP_API__.replay()', windowStub.__replayed === true)
|
|
503
512
|
/* v2.13.15 回归:选完「配音音色」直接点预览(还没点保存)也必须按当前选择播 */
|
|
504
513
|
tree = renderPanel()
|
|
505
|
-
const voiceSel = walk(tree).filter((n) => n.type === '
|
|
506
|
-
check('配音音色下拉能从 /voices 拿到音色G', !!voiceSel, JSON.stringify(walk(tree).filter((n) => n.type === '
|
|
514
|
+
const voiceSel = walk(tree).filter((n) => typeof n.type === 'function' && Array.isArray(n.props.options) && n.props.options.some((o) => o && o[0] === 'g'))[0]
|
|
515
|
+
check('配音音色下拉能从 /voices 拿到音色G', !!voiceSel, JSON.stringify(walk(tree).filter((n) => typeof n.type === 'function' && Array.isArray(n.props.options)).map((n) => n.props.options.map((o) => o && o[0]))))
|
|
507
516
|
if (voiceSel) {
|
|
508
|
-
voiceSel.props.onChange(
|
|
517
|
+
voiceSel.props.onChange('g')
|
|
509
518
|
await flush()
|
|
510
519
|
tree = renderPanel()
|
|
511
520
|
const previewBtn2 = find(tree, (n) => n.type === 'button' && n.props.children === '立即预览')
|
package/test/smoke-package.mjs
CHANGED
|
@@ -85,7 +85,8 @@ for (const f of ['index.html', 'render.html']) {
|
|
|
85
85
|
if (bigfishApp.includes('flashEl')) throw new Error('app.js 仍引用模板白闪 flashEl')
|
|
86
86
|
if (bigfishApp.includes('flashEl')) throw new Error('app.js 仍残留白闪动画')
|
|
87
87
|
if (skipCss.includes('#flash')) throw new Error('style.css 仍残留 #flash 样式')
|
|
88
|
-
if (!/
|
|
88
|
+
if (!/if \(typeof afterIframe === 'function'\) return afterIframe\(\)/.test(splashJs)) throw new Error('iframe 模板收尾未支持第二幕回调')
|
|
89
|
+
if (!/return done\.then\(function \(\) \{[\s\S]{0,420}transOut\(transType\('Out'\), finish\)/.test(splashJs)) throw new Error('iframe 模板收尾未交给插件退场转场')
|
|
89
90
|
/* v2.15.12:结尾等配音时圆环继续转 */
|
|
90
91
|
if (!bigfishApp.includes('const rt=t+waitSec')) throw new Error('结尾圆环未使用连续旋转时钟 rt')
|
|
91
92
|
if (!bigfishApp.includes('rotate(${rt*5})')) throw new Error('外圈未使用 rt 旋转')
|
|
@@ -170,4 +171,34 @@ if (!installPs1V2162.includes('npm_config_fetch_timeout')) throw new Error('inst
|
|
|
170
171
|
if (!installPs1V2162.includes('registry.npmmirror.com')) throw new Error('install.ps1 缺少国内镜像重试')
|
|
171
172
|
const helperGen = fs.readFileSync(path.join(root, 'tools/make-upgrade-helper.mjs'), 'utf8')
|
|
172
173
|
if (!helperGen.includes('turn off any proxy')) throw new Error('升级助手缺少代理提示')
|
|
174
|
+
/* v2.16.3:设置页下拉改自绘,跟随皮肤 3D 面板平面 */
|
|
175
|
+
if (!clientSrc.includes('__ss_selectMenu')) throw new Error('client.js 缺少自绘下拉菜单')
|
|
176
|
+
if (!clientSrc.includes('aria-haspopup')) throw new Error('client.js 自绘下拉缺少 aria 属性')
|
|
177
|
+
if (!clientSrc.includes('function CwSelect(')) throw new Error('client.js 聊天背景缺少 CwSelect')
|
|
178
|
+
if (clientSrc.includes("h('select',")) throw new Error('client.js 仍残留原生 select(设置页)')
|
|
179
|
+
if (clientSrc.includes("cwH('select',")) throw new Error('client.js 仍残留原生 select(聊天背景)')
|
|
180
|
+
/* v2.16.6:第一幕是 iframe 模板时也要接第二幕 */
|
|
181
|
+
if (!splashSrc.includes('function runIframeTemplate(tpl, afterIframe)')) throw new Error('splash.js 未给 iframe 模板留第二幕回调')
|
|
182
|
+
if (!splashSrc.includes('runIframeTemplate(TEMPLATE, function () {')) throw new Error('splash.js iframe 首幕未接第二幕')
|
|
183
|
+
if (!splashSrc.includes('afterIframe')) throw new Error('splash.js 缺少 afterIframe 处理')
|
|
184
|
+
/* v2.16.7:视频原声开关 + 掉线彩蛋底图 */
|
|
185
|
+
const hostSrcV2167 = fs.readFileSync(path.join(root, 'lib/host-startup.js'), 'utf8')
|
|
186
|
+
if (!hostSrcV2167.includes('act1VideoSound: false')) throw new Error('host-startup 缺少第一幕视频原声默认值')
|
|
187
|
+
if (!hostSrcV2167.includes('act2VideoSound: false')) throw new Error('host-startup 缺少第二幕视频原声默认值')
|
|
188
|
+
if (!hostSrcV2167.includes("'act1VideoSound'") || !hostSrcV2167.includes("'act2VideoSound'")) throw new Error('host-startup 的 BOOL_KEYS 缺少视频原声开关')
|
|
189
|
+
if (!splashSrc.includes('act2VideoSound === true')) throw new Error('splash.js 缺少第二幕视频原声分支')
|
|
190
|
+
if (!splashSrc.includes('fallback muted')) throw new Error('splash.js 缺少未静音自动播放失败回退')
|
|
191
|
+
if (!clientSrc.includes('第一幕视频原声') || !clientSrc.includes('第二幕视频原声')) throw new Error('client.js 缺少视频原声设置项')
|
|
192
|
+
const eggBg = path.join(root, 'templates/division-bigfish-codex/assets/egg-bg.png')
|
|
193
|
+
if (!fs.existsSync(eggBg)) throw new Error('缺少掉线彩蛋底图 egg-bg.png')
|
|
194
|
+
if (fs.statSync(eggBg).size < 1000000) throw new Error('掉线彩蛋底图疑似未替换(文件过小)')
|
|
195
|
+
/* v2.16.8:聊天背景第二张内置背景(可选,不改变默认) */
|
|
196
|
+
const wallSrcV2168 = fs.readFileSync(path.join(root, 'lib/host-wallpaper.js'), 'utf8')
|
|
197
|
+
if (!wallSrcV2168.includes('BUILTIN_IMAGES')) throw new Error('host-wallpaper 缺少多张内置背景表')
|
|
198
|
+
if (!wallSrcV2168.includes("'__snowgirl__'")) throw new Error('host-wallpaper 缺少第二张内置背景哨兵')
|
|
199
|
+
if (!wallSrcV2168.includes('builtinName')) throw new Error('host-wallpaper 缺少内置背景名称回传')
|
|
200
|
+
if (!clientSrc.includes('builtinName')) throw new Error('client.js 缺少内置背景名称显示')
|
|
201
|
+
const snowBg = path.join(root, 'lib/assets/builtin-background-snowgirl.png')
|
|
202
|
+
if (!fs.existsSync(snowBg)) throw new Error('缺少第二张内置背景图 builtin-background-snowgirl.png')
|
|
203
|
+
if (fs.statSync(snowBg).size < 1000000) throw new Error('第二张内置背景图疑似不完整')
|
|
173
204
|
console.log('ok package.json version=' + pkg.version + ',无 BOM,关键文件齐全(含更新前回滚 checkpoint)');
|
|
@@ -463,7 +463,7 @@ ok(intervals.every((t) => t.cleared) || intervals.length === 0, '旧的 interval
|
|
|
463
463
|
intervals.map((t) => (t.cleared ? 'cleared' : 'live')).join(','))
|
|
464
464
|
|
|
465
465
|
console.log('\n=== ⑦ v2.13.0 测试钩子 / BUILD ===')
|
|
466
|
-
ok(mod.__BUILD === 'cw-
|
|
466
|
+
ok(mod.__BUILD === 'cw-74', '__BUILD = cw-68(排查浏览器跑的是不是新包)', String(mod.__BUILD))
|
|
467
467
|
ok(mod.__VIDEO_ID === 'dsh-cw-video', '__VIDEO_ID = dsh-cw-video', String(mod.__VIDEO_ID))
|
|
468
468
|
ok(typeof mod.__startShuffle === 'function', '__startShuffle 导出')
|
|
469
469
|
ok(typeof mod.__stopShuffle === 'function', '__stopShuffle 导出')
|
|
@@ -196,10 +196,12 @@ try {
|
|
|
196
196
|
sortKeys(r.json))
|
|
197
197
|
check('/images 的 dropDir 指向临时 DSH_HOME',
|
|
198
198
|
r.json && r.json.dropDir.includes(HOME.replace(/\\/g, '/')), r.json && r.json.dropDir)
|
|
199
|
-
check('★ /images
|
|
200
|
-
r.json && Array.isArray(r.json.images) && r.json.images.length ===
|
|
199
|
+
check('★ /images 默认列出两张内置背景(默认图 selected,第二张可选)',
|
|
200
|
+
r.json && Array.isArray(r.json.images) && r.json.images.length === 2 &&
|
|
201
201
|
r.json.images[0].builtin === true && r.json.images[0].selected === true &&
|
|
202
|
-
r.json.images[0].url.includes('__default__')
|
|
202
|
+
r.json.images[0].url.includes('__default__') &&
|
|
203
|
+
r.json.images[1].builtin === true && r.json.images[1].selected === false &&
|
|
204
|
+
r.json.images[1].url.includes('__snowgirl__'),
|
|
203
205
|
JSON.stringify(r.json && r.json.images))
|
|
204
206
|
check('/images.imageSource = builtin、imageConfig 为空',
|
|
205
207
|
r.json && r.json.imageSource === 'builtin' && r.json.imageConfig === '',
|
|
@@ -323,9 +325,10 @@ try {
|
|
|
323
325
|
r.json && r.json.imageSource === 'your-library', r.json && r.json.imageSource)
|
|
324
326
|
|
|
325
327
|
r = await callJson('exact /chat-wallpaper/images')
|
|
326
|
-
check('★ 丢图后 /images 列出
|
|
327
|
-
r.json && Array.isArray(r.json.images) && r.json.images.length ===
|
|
328
|
-
r.json.images[0].builtin === true && r.json.images[1].
|
|
328
|
+
check('★ 丢图后 /images 列出 3 张:两张内置在前、用户的在后',
|
|
329
|
+
r.json && Array.isArray(r.json.images) && r.json.images.length === 3 &&
|
|
330
|
+
r.json.images[0].builtin === true && r.json.images[1].builtin === true &&
|
|
331
|
+
r.json.images[2].name === 'smoke-bg.png' && r.json.images[2].selected === true,
|
|
329
332
|
JSON.stringify(r.json && r.json.images))
|
|
330
333
|
check('丢图后 /images.current 指向它、imageSource = your-library',
|
|
331
334
|
r.json && typeof r.json.current === 'string' && r.json.current.endsWith('smoke-bg.png') &&
|
|
@@ -365,10 +365,10 @@ ok(MODEL.lastRootsBody && MODEL.lastRootsBody.action === 'remove' && String(MODE
|
|
|
365
365
|
const beforeMode = requests.length
|
|
366
366
|
tree = renderPanel()
|
|
367
367
|
const modeSel = findNode(tree, (n) =>
|
|
368
|
-
n.type === '
|
|
369
|
-
n.props.
|
|
368
|
+
typeof n.type === 'function' && Array.isArray(n.props.options) &&
|
|
369
|
+
n.props.options.some((o) => o && o[0] === 'video'))
|
|
370
370
|
ok(!!modeSel, '「背景类型」下拉含「循环视频」选项')
|
|
371
|
-
modeSel.props.onChange(
|
|
371
|
+
modeSel.props.onChange('video')
|
|
372
372
|
await flushP(); await flushP(); await flushP(); await flushP()
|
|
373
373
|
const modeReq = requests.slice(beforeMode).find((r) => r.url.includes('/chat-wallpaper/set'))
|
|
374
374
|
const modeBody = modeReq && JSON.parse(modeReq.init.body)
|