dsh-whale-widget 0.3.9 → 0.3.10
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/assets/whale-widget.js +290 -45
- package/lib/index.js +7 -5
- package/package.json +1 -1
package/assets/whale-widget.js
CHANGED
|
@@ -93,6 +93,32 @@ window.__dshWhaleInit = true
|
|
|
93
93
|
// 衔接时机由 RELEASE_LEAD_MS 决定(0 = 正好接上;30/50 = 轻微交叠)—— 改这个数字即可按耳朵微调,
|
|
94
94
|
// 不用动任何逻辑。
|
|
95
95
|
var dshwvAudioCtx = null
|
|
96
|
+
// v753(issue #135):**running 状态的 AudioContext 会让系统一直挂着 PreventUserIdleSystemSleep** ——
|
|
97
|
+
// macOS 上表现为"只要页面开着就不会空闲睡眠",而且与**有没有出声无关**(context 一 running 就持有播放流)。
|
|
98
|
+
// 所以空闲 DSHW_AUDIO_IDLE_MS 之后主动 suspend();下次出声前 dshwvAudio() 里的 resume() 会自动恢复。
|
|
99
|
+
// 为什么用 suspend() 而不是 close():close() 会把 context 彻底销毁,而**任务结束音不是手势触发的**,
|
|
100
|
+
// 销毁后它再也响不出来(被手势解锁过的 context 再 resume 不需要新手势,挂起是安全的)。
|
|
101
|
+
var DSHW_AUDIO_IDLE_MS = 60000 // 静默多久交还系统睡眠(用户选定:1 分钟)
|
|
102
|
+
var dshwvAudioSuspendT = null
|
|
103
|
+
function dshwvAudioSuspendNow() {
|
|
104
|
+
try {
|
|
105
|
+
if (dshwvAudioSuspendT) { clearTimeout(dshwvAudioSuspendT); dshwvAudioSuspendT = null }
|
|
106
|
+
if (dshwvAudioCtx && dshwvAudioCtx.state === 'running') dshwvAudioCtx.suspend()
|
|
107
|
+
} catch (err) {}
|
|
108
|
+
}
|
|
109
|
+
function dshwvAudioIdleArm() {
|
|
110
|
+
try {
|
|
111
|
+
if (dshwvAudioSuspendT) clearTimeout(dshwvAudioSuspendT)
|
|
112
|
+
dshwvAudioSuspendT = setTimeout(function () { dshwvAudioSuspendT = null; dshwvAudioSuspendNow() }, DSHW_AUDIO_IDLE_MS)
|
|
113
|
+
} catch (err) {}
|
|
114
|
+
}
|
|
115
|
+
// 音效是否被**显式**关掉(v753 的显式开关)。这里刻意用 typeof 保护:
|
|
116
|
+
// 本文件经常被探针按片段切出来单独 eval(例如 _v752-check 只切「音频垫片 + applySoundSet」),
|
|
117
|
+
// 那些沙箱里不一定声明了 soundOn —— typeof 对未声明的标识符是安全的,裸引用会直接抛
|
|
118
|
+
// ReferenceError 把整段沙箱打断。真机上 soundOn 一定存在,行为不受影响。
|
|
119
|
+
function dshwvSoundOff() {
|
|
120
|
+
try { return typeof soundOn !== 'undefined' && soundOn === false } catch (err) { return false }
|
|
121
|
+
}
|
|
96
122
|
function dshwvAudio() {
|
|
97
123
|
try {
|
|
98
124
|
if (!dshwvAudioCtx) {
|
|
@@ -101,24 +127,47 @@ function dshwvAudio() {
|
|
|
101
127
|
dshwvAudioCtx = new AC({ latencyHint: 'interactive' })
|
|
102
128
|
}
|
|
103
129
|
if (dshwvAudioCtx.state === 'suspended') { try { dshwvAudioCtx.resume() } catch (err) {} }
|
|
130
|
+
dshwvAudioIdleArm() // 每次(重新)进入 running 都重新计时:静默满 1 分钟就挂起
|
|
104
131
|
return dshwvAudioCtx
|
|
105
132
|
} catch (err) { return null }
|
|
106
133
|
}
|
|
107
134
|
var dshwvAudioBuffers = {} // url -> Promise<AudioBuffer>(同一片段不重复下载/解码)
|
|
108
135
|
var dshwvAudioDecoded = {} // url -> AudioBuffer(解码完成后**同步可读**:起播走同步路径的关键)
|
|
136
|
+
// v752:音频失败**必须看得见**。原来所有 fetch/decode 失败都被 `.catch(function(){})` 吞掉,
|
|
137
|
+
// 于是"没声音"在控制台里一点痕迹都没有,只能靠猜。现在每个 URL 只 warn 一次,
|
|
138
|
+
// 并把最后一次错误留在 dshwvAudioLastErr 里。
|
|
139
|
+
var dshwvAudioLastErr = null
|
|
140
|
+
var dshwvAudioWarned = {}
|
|
141
|
+
function dshwvAudioWarn(url, err) {
|
|
142
|
+
var msg = String((err && err.message) || err || 'unknown')
|
|
143
|
+
dshwvAudioLastErr = { url: String(url || ''), error: msg, at: new Date().toISOString() }
|
|
144
|
+
if (dshwvAudioWarned[url]) return
|
|
145
|
+
dshwvAudioWarned[url] = 1
|
|
146
|
+
try { console.warn('[小鲸鱼] 音频加载/解码失败:' + url + ' → ' + msg) } catch (e) {}
|
|
147
|
+
}
|
|
109
148
|
function nowMs() { try { return (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now() } catch (err) { return Date.now() } }
|
|
110
149
|
function dshwvAudioBuffer(url) {
|
|
111
150
|
if (!url) return Promise.reject(new Error('empty url'))
|
|
112
151
|
if (!dshwvAudioBuffers[url]) {
|
|
113
|
-
|
|
114
|
-
|
|
152
|
+
// v752:改 no-store。原来用 force-cache —— 万一某个中间层把一次空的 204 缓存住,
|
|
153
|
+
// 之后每次点按都会复用它(表现就是"永久没声音,重启也没用")。我们本来就有内存解码缓存,
|
|
154
|
+
// 不会因此重复下载。
|
|
155
|
+
dshwvAudioBuffers[url] = fetch(url, { cache: 'no-store' })
|
|
156
|
+
.then(function (r) {
|
|
157
|
+
// 注意:204 也满足 r.ok(2xx),必须单独挡掉,否则 0 字节会被送进 decodeAudioData
|
|
158
|
+
// → 抛 EncodingError("Unable to decode audio data")→ 只剩静音。
|
|
159
|
+
if (r.status === 204) throw new Error('HTTP 204(无内容:该音效槽留空、或音效组未找到)')
|
|
160
|
+
if (!r.ok) throw new Error('HTTP ' + r.status)
|
|
161
|
+
return r.arrayBuffer()
|
|
162
|
+
})
|
|
115
163
|
.then(function (raw) {
|
|
164
|
+
if (!raw || raw.byteLength < 100) throw new Error('音频响应只有 ' + (raw ? raw.byteLength : 0) + ' 字节(不是有效音频)')
|
|
116
165
|
var c = dshwvAudio()
|
|
117
166
|
if (!c) throw new Error('no audio context')
|
|
118
167
|
return new Promise(function (res, rej) { c.decodeAudioData(raw, res, rej) })
|
|
119
168
|
})
|
|
120
169
|
.then(function (buf) { dshwvAudioDecoded[url] = buf; return buf })
|
|
121
|
-
.catch(function (err) { delete dshwvAudioBuffers[url]; delete dshwvAudioDecoded[url]; throw err })
|
|
170
|
+
.catch(function (err) { delete dshwvAudioBuffers[url]; delete dshwvAudioDecoded[url]; dshwvAudioWarn(url, err); throw err })
|
|
122
171
|
}
|
|
123
172
|
return dshwvAudioBuffers[url]
|
|
124
173
|
}
|
|
@@ -126,6 +175,9 @@ function dshwvAudioBuffer(url) {
|
|
|
126
175
|
// 不预热 → 第一次点按要现 fetch+decodeAudioData,表现就是"按下音慢半拍、中间衔接发飘"。
|
|
127
176
|
// URL 级缓存保证同一片段只解一次;失败静默吞掉(真正播放时还会自己重试一次)。
|
|
128
177
|
function dshwvWarm(urls) {
|
|
178
|
+
// v753(issue #135):音效关掉时**连预热都不做** —— 只跳过下面那句 dshwvAudio() 是不够的,
|
|
179
|
+
// 因为预解码走到 dshwvAudioBuffer() 里还会再调一次 dshwvAudio(),context 照样被建起来。
|
|
180
|
+
if (dshwvSoundOff()) return
|
|
129
181
|
try { dshwvAudio() } catch (err) {}
|
|
130
182
|
for (var i = 0; i < (urls || []).length; i++) {
|
|
131
183
|
var u = urls[i]
|
|
@@ -187,9 +239,15 @@ function dshwvSound(url) {
|
|
|
187
239
|
el._playingSince = nowMs() + delay * 1000
|
|
188
240
|
var dur = Math.max(0.001, buf.duration)
|
|
189
241
|
src.start(delay > 0 ? c.currentTime + delay : 0, Math.max(0, el._offset) % dur)
|
|
190
|
-
} catch (err) {
|
|
242
|
+
} catch (err) {
|
|
243
|
+
// v749:起播抛异常(例如 AudioContext 已关闭、offset 越界)以前被完全吞掉 → 表现为"没声音但无任何报错"
|
|
244
|
+
dshwvAudioWarn(el && el._url, err)
|
|
245
|
+
}
|
|
191
246
|
}
|
|
192
247
|
function startSound(delaySec) {
|
|
248
|
+
// v753(issue #135):音效开关关掉 = 全静音(含编辑器里的试听),也**不碰** AudioContext ——
|
|
249
|
+
// 否则一次试听就会把 context 转成 running、系统断言照挂。
|
|
250
|
+
if (dshwvSoundOff()) return Promise.resolve()
|
|
193
251
|
var c = dshwvAudio()
|
|
194
252
|
if (!c || !el._url) return Promise.resolve()
|
|
195
253
|
var token = (el._token = (el._token || 0) + 1)
|
|
@@ -197,27 +255,47 @@ function dshwvSound(url) {
|
|
|
197
255
|
// ④ 同步起播:缓冲区已预热(dshwvWarm)过 → 直接在当前任务里 start(),不再等 Promise
|
|
198
256
|
var cached = dshwvAudioDecoded[el._url]
|
|
199
257
|
if (cached) { beginWithBuffer(c, cached, delay); return Promise.resolve() }
|
|
200
|
-
|
|
258
|
+
var tryUrl = el._url
|
|
259
|
+
dshwvAudioBuffer(tryUrl).then(function (buf) {
|
|
201
260
|
if (token !== el._token) return // 期间被重播/暂停/换源 → 丢弃这次
|
|
202
261
|
beginWithBuffer(c, buf, delay)
|
|
203
|
-
}).catch(function () {
|
|
262
|
+
}).catch(function () {
|
|
263
|
+
// v752:首选路由取不到音频时,自动换另一条路由重试一次(两条互为备胎)。
|
|
264
|
+
// 只在失败路径上跑,不影响正常点按的同步起播时序。
|
|
265
|
+
if (!el._alt || el._url !== tryUrl || token !== el._token) return
|
|
266
|
+
var alt = el._alt
|
|
267
|
+
el._alt = '' // 只回退一次,避免两条路由来回打转
|
|
268
|
+
el._url = alt
|
|
269
|
+
try { dshwvWarm([alt]) } catch (e) {} // 后台预热,下次点按就能走同步路径
|
|
270
|
+
try { startSound(delay) } catch (e) {}
|
|
271
|
+
})
|
|
204
272
|
return Promise.resolve()
|
|
205
273
|
}
|
|
206
274
|
el.play = function () { return startSound(0) }
|
|
207
275
|
el.playAt = function (delaySec) { return startSound(delaySec) }
|
|
208
276
|
el.pause = function () { dshwvSoundStop(el) }
|
|
277
|
+
// v752:备用路由(首选失败时自动切过去);由 applySoundSet/playTaskEndGroupClick 填
|
|
278
|
+
el._alt = ''
|
|
209
279
|
return el
|
|
210
280
|
}
|
|
281
|
+
|
|
211
282
|
// 自动播放策略:AudioContext 初始是 suspended,要有一次用户手势才能出声;任务结束音不是手势触发的,
|
|
212
283
|
// 所以挂一次性解锁(首次点击/按键后移除)。
|
|
213
284
|
try {
|
|
214
285
|
var dshwvAudioUnlock = function () {
|
|
286
|
+
// v753(issue #135):音效关掉时**不预解锁** —— 否则一次普通点击就会把 context 转成 running,
|
|
287
|
+
// 断言照挂。注意这里**不摘监听**:之后重新打开开关,下一次点击仍能完成解锁。
|
|
288
|
+
if (dshwvSoundOff()) return
|
|
215
289
|
dshwvAudio()
|
|
216
290
|
try { document.removeEventListener('pointerdown', dshwvAudioUnlock, true) } catch (err) {}
|
|
217
291
|
try { document.removeEventListener('keydown', dshwvAudioUnlock, true) } catch (err) {}
|
|
218
292
|
}
|
|
219
293
|
document.addEventListener('pointerdown', dshwvAudioUnlock, true)
|
|
220
294
|
document.addEventListener('keydown', dshwvAudioUnlock, true)
|
|
295
|
+
// v753(issue #135):页面被隐藏(切标签 / 最小化)时立刻挂起,直接覆盖"开着过夜"这个场景
|
|
296
|
+
document.addEventListener('visibilitychange', function () {
|
|
297
|
+
try { if (document.hidden) dshwvAudioSuspendNow() } catch (err) {}
|
|
298
|
+
})
|
|
221
299
|
} catch (err) {}
|
|
222
300
|
|
|
223
301
|
var MIN_SCALE = 0.6
|
|
@@ -245,10 +323,29 @@ var css = [
|
|
|
245
323
|
'.dshwv-root.dshwv-dragging{cursor:grabbing;transition:none}',
|
|
246
324
|
'.dshwv-body{position:absolute;left:0;top:0;width:100%;height:100%;transform-origin:50% 100%;transition:transform .22s cubic-bezier(.34,1.56,.64,1)}',
|
|
247
325
|
'.dshwv-img{position:absolute;right:0;bottom:0;width:59.45%;height:59.45%;display:block;pointer-events:none;-webkit-user-drag:none;user-select:none;object-fit:contain;object-position:right bottom}',
|
|
326
|
+
// v751(PR #119):光标不再写 document.body.style.cursor —— cursor 是可继承属性,写 <body> 会让 Blink
|
|
327
|
+
// 失效**整棵文档树**的样式;而它在点击链路上按下/抬手各写一次,紧接着 isWhaleHit() 的
|
|
328
|
+
// getBoundingClientRect() 与泡泡行测量的 getComputedStyle()/scrollWidth 会强制刷新样式+布局,
|
|
329
|
+
// 于是"整页样式重算"被算进了这一次点击(长会话里表现为桌宠点了卡一下、松手音延迟、跑马灯不动)。
|
|
330
|
+
// 现在光标由挂件自己的类承担:命中鲸鱼不透明像素时让 .dshwv-img 接过指针并显示 grab / grabbing。
|
|
331
|
+
// 鲸鱼区域本来就在吞指针事件(onDocPointerDown 的 isWhaleHit),这不是新增拦截;
|
|
332
|
+
// 新增影响的只有滚轮,由 onWhaleWheel 转交给指针下方真正可滚动的容器。
|
|
333
|
+
'.dshwv-root.dshwv-cursor-grab .dshwv-img{pointer-events:auto;cursor:grab}',
|
|
334
|
+
'.dshwv-root.dshwv-cursor-grabbing .dshwv-img{pointer-events:auto;cursor:grabbing}',
|
|
335
|
+
// 拖动期间握点相对挂件固定,让整个盒接过指针以保持 grabbing(类由按下时加、endDrag 摘)
|
|
336
|
+
'.dshwv-root.dshwv-dragging .dshwv-body{pointer-events:auto;cursor:grabbing}',
|
|
248
337
|
'.dshwv-pop{position:absolute;left:0;top:0;width:100%;aspect-ratio:1026/700;pointer-events:none;z-index:1;--dshw-u:calc(var(--dshw-base) / 1026)}',
|
|
249
338
|
// 纵深防御:泡泡容器必须透明,形状由内部 SVG 绘制;用 !important 压掉外部
|
|
250
339
|
// 插件“类名子串匹配”选择器(如 aqua 的 [class*=bubble])注入的玻璃/边框样式
|
|
251
340
|
'html .dshwv-pop, html .dshwv-pop svg{background:transparent !important;border:0 !important;border-radius:0 !important;backdrop-filter:none !important;-webkit-backdrop-filter:none !important}',
|
|
341
|
+
// 纵深防御(issue #133):宿主皮肤/主题会按**几何特征**(position:fixed + z-index≥10 +
|
|
342
|
+
// 尺寸≥120×80 + 非原生弹窗角色)把挂件方块误判成"独立插件窗",然后注入
|
|
343
|
+
// backdrop-filter / 背景 / 边框 / 伪元素毛玻璃,把用户壁纸糊掉(实测 375×375 方块里是雾面)。
|
|
344
|
+
// 挂件是透明精灵图,根节点上任何"画底"的通道都不该存在,所以这里全部钉死为透明 ——
|
|
345
|
+
// 我们自己的 .dshwv-root 本来就只有 left/top/width/height/transform,没有背景/边框/阴影/filter,
|
|
346
|
+
// 也没有用 ::before/::after,所以这是零行为变化的加固。
|
|
347
|
+
'html .dshwv-root,body .dshwv-root{background:transparent !important;background-image:none !important;border:0 !important;outline:0 !important;box-shadow:none !important;backdrop-filter:none !important;-webkit-backdrop-filter:none !important;filter:none !important}',
|
|
348
|
+
'html .dshwv-root::before,html .dshwv-root::after{content:none !important;background:transparent !important;background-image:none !important;box-shadow:none !important;backdrop-filter:none !important;-webkit-backdrop-filter:none !important}',
|
|
252
349
|
'.dshwv-pop svg{display:block;width:100%;height:100%;pointer-events:none}',
|
|
253
350
|
'.dshwv-pop svg path,.dshwv-pop svg ellipse{pointer-events:none;cursor:pointer}',
|
|
254
351
|
'.dshwv-pop.dshwv-pop-open svg path,.dshwv-pop.dshwv-pop-open svg ellipse{pointer-events:visiblePainted}',
|
|
@@ -1213,14 +1310,19 @@ function playTaskEndSound() {
|
|
|
1213
1310
|
if (!usageSet || !usageSet.taskEnd || !usageSet.taskEnd.on || soundOn === false) return
|
|
1214
1311
|
var sel = usageSet.taskEnd.sel || taskEndSel.value || ''
|
|
1215
1312
|
var url = ''
|
|
1313
|
+
var altUrl = ''
|
|
1216
1314
|
if (sel.indexOf('grp:') === 0) { playTaskEndGroupClick(sel.slice(4)); return }
|
|
1217
1315
|
if (sel.indexOf('frag:') === 0) url = '/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(sel.slice(5))
|
|
1218
1316
|
else if (sel.indexOf('preset:') === 0) {
|
|
1317
|
+
// v752:预设片段也改走片段路由(与本体按压同一套选择逻辑 + 备用路由)
|
|
1219
1318
|
var parts = sel.split(':')
|
|
1220
|
-
|
|
1319
|
+
var su = soundSlotUrls(parts[2] === 'release' ? 'release' : 'press', parts[1])
|
|
1320
|
+
url = su.url
|
|
1321
|
+
altUrl = su.alt || ''
|
|
1221
1322
|
}
|
|
1222
1323
|
if (!url) return
|
|
1223
1324
|
var a = dshwvSound(url)
|
|
1325
|
+
if (altUrl) a._alt = altUrl
|
|
1224
1326
|
try { a.volume = Number(soundVol) || 0.9 } catch (err) {}
|
|
1225
1327
|
a.play().catch(function () {})
|
|
1226
1328
|
} catch (err) {}
|
|
@@ -1236,11 +1338,17 @@ function playTaskEndGroupClick(groupId) {
|
|
|
1236
1338
|
var releaseEmpty = !!(g && g.release === '')
|
|
1237
1339
|
if (pressEmpty && releaseEmpty) return
|
|
1238
1340
|
var vol = Number(soundVol) || 0.9
|
|
1341
|
+
// v752:任务结束音同样改走片段路由(与本体按压同一套 URL 选择 + 备用路由逻辑)——
|
|
1342
|
+
// 老路由 /dsh-whale/sound/*.mp3?set=… 在部分环境会被本机那层东西拦成空的 204,
|
|
1343
|
+
// 不改的话"点按有声、但每轮结束音没声"会变成同一个问题的另一半。
|
|
1344
|
+
var pu = soundSlotUrls('press', groupId)
|
|
1345
|
+
var ru = soundSlotUrls('release', groupId)
|
|
1239
1346
|
// 按压留空:无按下音,直接播松开(模拟按下即松开的完整点按);松开留空:只播按压
|
|
1240
1347
|
if (pressEmpty) {
|
|
1241
|
-
if (!releaseEmpty) {
|
|
1242
|
-
dshwvWarm([
|
|
1243
|
-
var relOnly = dshwvSound(
|
|
1348
|
+
if (!releaseEmpty && ru.url) {
|
|
1349
|
+
dshwvWarm([ru.url]) // v745:先预热
|
|
1350
|
+
var relOnly = dshwvSound(ru.url)
|
|
1351
|
+
relOnly._alt = ru.alt || ''
|
|
1244
1352
|
try { relOnly.volume = vol } catch (err) {}
|
|
1245
1353
|
relOnly.currentTime = 0
|
|
1246
1354
|
var pr = relOnly.play()
|
|
@@ -1248,20 +1356,20 @@ function playTaskEndGroupClick(groupId) {
|
|
|
1248
1356
|
}
|
|
1249
1357
|
return
|
|
1250
1358
|
}
|
|
1251
|
-
|
|
1359
|
+
if (!pu.url) return
|
|
1360
|
+
var press = dshwvSound(pu.url)
|
|
1361
|
+
press._alt = pu.alt || ''
|
|
1252
1362
|
try { press.volume = vol } catch (err) {}
|
|
1253
1363
|
// v745:菜单里的"点一下试听"同样先预热解码,否则第一次听有明显延迟
|
|
1254
|
-
dshwvWarm([
|
|
1255
|
-
|
|
1256
|
-
releaseEmpty ? '' : '/dsh-whale/sound/release.mp3?set=' + encodeURIComponent(groupId),
|
|
1257
|
-
])
|
|
1258
|
-
if (releaseEmpty) {
|
|
1364
|
+
dshwvWarm([pu.url, releaseEmpty ? '' : ru.url])
|
|
1365
|
+
if (releaseEmpty || !ru.url) {
|
|
1259
1366
|
press.currentTime = 0
|
|
1260
1367
|
var pp = press.play()
|
|
1261
1368
|
if (pp && pp.catch) pp.catch(function () {})
|
|
1262
1369
|
return
|
|
1263
1370
|
}
|
|
1264
|
-
var release = dshwvSound(
|
|
1371
|
+
var release = dshwvSound(ru.url)
|
|
1372
|
+
release._alt = ru.alt || ''
|
|
1265
1373
|
try { release.volume = vol } catch (err) {}
|
|
1266
1374
|
var relPlayed = false
|
|
1267
1375
|
function playRel() {
|
|
@@ -1335,8 +1443,17 @@ var volPct = document.createElement('span')
|
|
|
1335
1443
|
volPct.className = 'dshwv-volpct'
|
|
1336
1444
|
volPct.textContent = '90%'
|
|
1337
1445
|
volInput.addEventListener('input', function () { setVol(volInput.value) })
|
|
1446
|
+
// v753(issue #135):显式「音效开关」,放在「音量」右边。滑块是 flex:1、开关是 flex:0 0 auto(16px),
|
|
1447
|
+
// 所以「滑块 + 开关」合起来仍占原来滑块那一条宽度(缩掉的正好是开关与行间距的宽度),行总宽不变。
|
|
1448
|
+
var soundToggle = document.createElement('input')
|
|
1449
|
+
soundToggle.type = 'checkbox'
|
|
1450
|
+
soundToggle.className = 'dshwv-check'
|
|
1451
|
+
soundToggle.checked = true
|
|
1452
|
+
soundToggle.title = '音效总开关:关掉后不出声(含音效组试听),并立刻挂起音频上下文、把系统睡眠交还给你'
|
|
1453
|
+
soundToggle.addEventListener('change', function () { setSoundOn(soundToggle.checked) })
|
|
1338
1454
|
var row3 = menuRow()
|
|
1339
1455
|
row3.appendChild(menuLabel('音量'))
|
|
1456
|
+
row3.appendChild(soundToggle)
|
|
1340
1457
|
row3.appendChild(volInput)
|
|
1341
1458
|
row3.appendChild(volPct)
|
|
1342
1459
|
var row6 = menuRow()
|
|
@@ -8570,12 +8687,22 @@ function visibleTopZ() {
|
|
|
8570
8687
|
var top = 20500
|
|
8571
8688
|
// 注意:这里必须列全 —— 少一个浮层,dshwLayerUp/下拉/提示就会低估"当前最高层"而被盖住。
|
|
8572
8689
|
// toast 刻意不列入(它是永远的最顶层,不该让别的层去追它)。
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8690
|
+
// v750(issue #131 的教训):每个候选写成**独立取值函数**并各自 try/catch。
|
|
8691
|
+
// 0.3.8 曾在这里写了一个并不存在的 `usageMask` —— 数组字面量在**构造时**就抛 ReferenceError,
|
|
8692
|
+
// 于是 visibleTopZ() 每次调用都失败、又被调用方的空 catch 吞掉:自绘下拉的层级停在下拉样式表里的
|
|
8693
|
+
// 60(被父窗口整层盖住,表现为"点了没反应"),dshwLayerUp() 也一起静默失效。
|
|
8694
|
+
// 现在单个名字写错最多只少算那一层,不会让整条层级链失效;`_z-audit-check.mjs` 会核对
|
|
8695
|
+
// 候选表里的每个标识符都真的声明过(防止同样的错再犯)。
|
|
8696
|
+
var getters = [
|
|
8697
|
+
function () { return bubbleMask }, function () { return bubbleItemMask },
|
|
8698
|
+
function () { return moduleMask }, function () { return moduleNamePromptMask },
|
|
8699
|
+
function () { return cropMask }, function () { return gifMask },
|
|
8700
|
+
function () { return audioCropMask }, function () { return audioEditMask },
|
|
8701
|
+
function () { return resMaskEl }, function () { return confirmMask },
|
|
8702
|
+
function () { return snapMask }, function () { return usageMoreMask },
|
|
8703
|
+
function () { return qeditEl }, function () { return dshwvTplHelpEl },
|
|
8704
|
+
function () { return dshwvHintEl }, function () { return apiModelMaskEl },
|
|
8705
|
+
function () { return accountingMask }, function () { return window.__dshwRemindMask }
|
|
8579
8706
|
]
|
|
8580
8707
|
function eff(el) {
|
|
8581
8708
|
try {
|
|
@@ -8590,8 +8717,9 @@ function visibleTopZ() {
|
|
|
8590
8717
|
return isFinite(n) ? n : 0
|
|
8591
8718
|
} catch (err) { return 0 }
|
|
8592
8719
|
}
|
|
8593
|
-
for (var i = 0; i <
|
|
8594
|
-
var n =
|
|
8720
|
+
for (var i = 0; i < getters.length; i++) {
|
|
8721
|
+
var n = 0
|
|
8722
|
+
try { n = eff(getters[i]()) } catch (err) { n = 0 }
|
|
8595
8723
|
if (n > top) top = n
|
|
8596
8724
|
}
|
|
8597
8725
|
return top
|
|
@@ -8600,7 +8728,9 @@ function visibleTopZ() {
|
|
|
8600
8728
|
// 用在裁剪 / GIF / 音频裁剪 / 模块编辑器这些**既可能从主菜单(10000)打开、也可能从资源管理(20300)、
|
|
8601
8729
|
// 泡泡编辑器(20500)、模型设置(29000)里打开**的窗口上 —— 固定层号在后者场景会被父窗口盖住。
|
|
8602
8730
|
function dshwLayerUp(el, base) {
|
|
8603
|
-
|
|
8731
|
+
var z = base
|
|
8732
|
+
try { z = Math.max(base, Math.round(visibleTopZ()) + 10) } catch (err) { z = base }
|
|
8733
|
+
try { if (el && el.style) el.style.zIndex = String(z) } catch (err) {}
|
|
8604
8734
|
return el
|
|
8605
8735
|
}
|
|
8606
8736
|
// 打开主要编辑器前清理可能残留的临时层级(提醒会话遗留的 moduleMask/qedit 提升与样式)
|
|
@@ -8648,7 +8778,10 @@ function dshwDropOpen(menuEl, anchorEl) {
|
|
|
8648
8778
|
menuEl.style.left = Math.round(left) + 'px'
|
|
8649
8779
|
menuEl.style.top = Math.round(r.bottom + 2) + 'px'
|
|
8650
8780
|
// 下拉层级:高于当前所有可见弹窗/窗口(兜底不低于 26010)
|
|
8651
|
-
|
|
8781
|
+
// v750(issue #131):visibleTopZ() 单独 try/catch —— 算不出最高层也必须把层级抬上去,
|
|
8782
|
+
// 并且打一条 warn(原先异常被外层空 catch 吞掉,表现为"下拉点了没反应"且毫无线索)
|
|
8783
|
+
var vTop = 20500
|
|
8784
|
+
try { vTop = visibleTopZ() } catch (err) { try { console.warn('[dsh-whale] visibleTopZ 失败,下拉改用兜底层级:', err) } catch (e2) {} }
|
|
8652
8785
|
menuEl.style.zIndex = String(Math.max(26010, Math.round(vTop) + 10))
|
|
8653
8786
|
} catch (err) {}
|
|
8654
8787
|
}
|
|
@@ -10374,7 +10507,12 @@ function measureBubbleCenter() {
|
|
|
10374
10507
|
dshwCenterX = cx
|
|
10375
10508
|
dshwCenterY = cy
|
|
10376
10509
|
try {
|
|
10377
|
-
|
|
10510
|
+
// v751(PR #119):写到挂件自己的 root 上,**不写 document.documentElement**。
|
|
10511
|
+
// 未注册的自定义属性写在 <html> 上会让 Blink 保守失效整棵子树样式(PR 实测 12k 节点会话
|
|
10512
|
+
// 一次约 160ms),而本函数在初始化、rAF、load 以及每次窗口 resize 都会跑。
|
|
10513
|
+
// 消费这两个变量的 .dshwv-text/.dshwv-gif 都在 root 内(继承即可);
|
|
10514
|
+
// 编辑器预览在 root 之外,所以在 bubblePreviewInto() 里补写了两行。
|
|
10515
|
+
var s = root.style
|
|
10378
10516
|
s.setProperty('--dshw-vx', cx + '%')
|
|
10379
10517
|
s.setProperty('--dshw-vy', cy + '%')
|
|
10380
10518
|
} catch (err) {}
|
|
@@ -12141,6 +12279,12 @@ function bubblePreviewInto(container, mods, widthPx) {
|
|
|
12141
12279
|
container.style.transform = 'none'
|
|
12142
12280
|
container.style.transformOrigin = ''
|
|
12143
12281
|
container.style.setProperty('--dshw-u', (W / 1026) + 'px')
|
|
12282
|
+
// v751(PR #119):预览节点在挂件 root 之外,拿不到 root 上的 --dshw-vx/--dshw-vy(以前写在 <html> 上才吃得到),
|
|
12283
|
+
// 这里补一份,保证编辑器里的预览与真实泡泡同排版。
|
|
12284
|
+
try {
|
|
12285
|
+
container.style.setProperty('--dshw-vx', dshwCenterX + '%')
|
|
12286
|
+
container.style.setProperty('--dshw-vy', dshwCenterY + '%')
|
|
12287
|
+
} catch (err) {}
|
|
12144
12288
|
// 视觉右移 10px:用左右 margin 的非对称(右侧少让),避免溢出撑出横向滚动条
|
|
12145
12289
|
var halfGap = Math.max(0, (hostW - W) / 2)
|
|
12146
12290
|
var shiftR = Math.min(10, Math.max(0, Math.round(halfGap)))
|
|
@@ -12861,7 +13005,9 @@ function setScale(v) {
|
|
|
12861
13005
|
function setVol(v) {
|
|
12862
13006
|
var next = Math.round(Math.min(1, Math.max(0, Number(v))) * 100) / 100
|
|
12863
13007
|
soundVol = next
|
|
12864
|
-
|
|
13008
|
+
// v753(issue #135):**不再由音量派生 soundOn**。原来"拉到 0 就等于关音效"是个没写明的隐含行为,
|
|
13009
|
+
// 既让想临时静音的人被迫牺牲原来的音量值,也让"音效开关"这件事在界面上无迹可寻。
|
|
13010
|
+
// 现在开关自己说了算(见 setSoundOn),音量只表示音量。
|
|
12865
13011
|
volInput.value = String(next)
|
|
12866
13012
|
volPct.textContent = Math.round(next * 100) + '%'
|
|
12867
13013
|
try {
|
|
@@ -12870,6 +13016,22 @@ function setVol(v) {
|
|
|
12870
13016
|
} catch (err) {}
|
|
12871
13017
|
saveConfig()
|
|
12872
13018
|
}
|
|
13019
|
+
// v753(issue #135):显式音效总开关(菜单「音量」右边那个勾选框)。
|
|
13020
|
+
// 它同时管三件事:① 出声与否(播放路径都看 soundOn);② 关掉时**立刻挂起** AudioContext,
|
|
13021
|
+
// 把系统睡眠交还给用户,而不是等 1 分钟空闲;③ 重新打开时立刻预热,恢复"点按即响"的跟手度。
|
|
13022
|
+
function setSoundOn(v) {
|
|
13023
|
+
soundOn = v !== false
|
|
13024
|
+
try { soundToggle.checked = soundOn } catch (err) {}
|
|
13025
|
+
if (!soundOn) {
|
|
13026
|
+
dshwvAudioSuspendNow()
|
|
13027
|
+
} else {
|
|
13028
|
+
try { applySoundSet() } catch (err) {}
|
|
13029
|
+
}
|
|
13030
|
+
try {
|
|
13031
|
+
var p = saveConfig()
|
|
13032
|
+
if (p && typeof p.then === 'function') p.catch(function () {})
|
|
13033
|
+
} catch (err) {}
|
|
13034
|
+
}
|
|
12873
13035
|
function setSoundSet(v) {
|
|
12874
13036
|
// 支持预设组(duck/fx1)和自定义组 id
|
|
12875
13037
|
soundSet = typeof v === 'string' && v ? v : 'duck'
|
|
@@ -12884,6 +13046,28 @@ var pressing = false
|
|
|
12884
13046
|
var pressEnded = false
|
|
12885
13047
|
var releasePlayed = false
|
|
12886
13048
|
// v745:不再需要 releaseTimer —— 点按时由 playReleaseAt() 在**音频线程**排期(见 pressUp)
|
|
13049
|
+
// —— v752:本体按压/松开音改走「片段路由」——
|
|
13050
|
+
// 起因:issue 里那台机器上 /dsh-whale/sound/press.mp3?set=… 会被本机的一层东西(代理/安全软件)
|
|
13051
|
+
// 拦成**空的 204**(响应头里没有 Date、还多出 pragma: no-cache,不是本进程发出的),
|
|
13052
|
+
// 0 字节送进 decodeAudioData 就抛 EncodingError → 只剩静音;
|
|
13053
|
+
// 而同一台机器上 /dsh-whale/audio-fragment.wav?id=<片段> 是 200 且能解码。
|
|
13054
|
+
// 两边的字节是**同一个文件**(预设 ya1→assets/Ya1.mp3;自定义组→它引用的那个片段),
|
|
13055
|
+
// 所以音色、时长、衔接时序都不变;同时少一层"组→片段"的宿主侧间接。
|
|
13056
|
+
// 返回 { url, alt, empty }:url=首选(片段路由)、alt=兜底(老的声音组路由,失败时自动切换)、
|
|
13057
|
+
// empty=true 表示该槽显式留空('' = 该事件静音,按设计不出声)。
|
|
13058
|
+
function soundSlotUrls(slot, groupId) {
|
|
13059
|
+
var gid = String(groupId || soundSet || 'duck')
|
|
13060
|
+
if (audioGroupSlotEmpty(gid, slot)) return { url: '', alt: '', empty: true }
|
|
13061
|
+
var frag = ''
|
|
13062
|
+
for (var i = 0; i < audioGroups.length; i++) {
|
|
13063
|
+
var g = audioGroups[i]
|
|
13064
|
+
if (g && g.id === gid) { frag = String(g[slot] || ''); break }
|
|
13065
|
+
}
|
|
13066
|
+
var legacy = '/dsh-whale/sound/' + (slot === 'press' ? 'press' : 'release') + '.mp3?set=' + encodeURIComponent(gid)
|
|
13067
|
+
// 组信息还没到(audio.json 未返回)或找不到组 → 与旧行为完全一致,走老路由
|
|
13068
|
+
if (!frag) return { url: legacy, alt: '', empty: false }
|
|
13069
|
+
return { url: '/dsh-whale/audio-fragment.wav?id=' + encodeURIComponent(frag), alt: legacy, empty: false }
|
|
13070
|
+
}
|
|
12887
13071
|
function applySoundSet() {
|
|
12888
13072
|
try {
|
|
12889
13073
|
// v729:切音效组 / 开关音效时把本轮播放状态一并复位,
|
|
@@ -12891,24 +13075,25 @@ function applySoundSet() {
|
|
|
12891
13075
|
pressEnded = false
|
|
12892
13076
|
releasePlayed = false
|
|
12893
13077
|
// 槽位显式留空(该事件静音)时,对应音频元素置空;playPress/playRelease 已判空
|
|
12894
|
-
var
|
|
12895
|
-
var
|
|
12896
|
-
if (
|
|
12897
|
-
pressAudio = dshwvSound(
|
|
13078
|
+
var pu = soundSlotUrls('press')
|
|
13079
|
+
var ru = soundSlotUrls('release')
|
|
13080
|
+
if (pu.empty) { pressAudio = null } else {
|
|
13081
|
+
pressAudio = dshwvSound(pu.url)
|
|
13082
|
+
pressAudio._alt = pu.alt || ''
|
|
12898
13083
|
pressAudio.preload = 'auto'
|
|
12899
13084
|
pressAudio.volume = soundVol
|
|
12900
13085
|
}
|
|
12901
|
-
if (
|
|
12902
|
-
releaseAudio = dshwvSound(
|
|
13086
|
+
if (ru.empty) { releaseAudio = null } else {
|
|
13087
|
+
releaseAudio = dshwvSound(ru.url)
|
|
13088
|
+
releaseAudio._alt = ru.alt || ''
|
|
12903
13089
|
releaseAudio.preload = 'auto'
|
|
12904
13090
|
releaseAudio.volume = soundVol
|
|
12905
13091
|
}
|
|
12906
13092
|
// v745:把这组的按压/松开音**预取+预解码**(Web Audio 下 preload='auto' 不解码)。
|
|
12907
13093
|
// 预热过之后,起播走 dshwvSound 的同步路径(pointerdown 同一任务里 start),手感才贴手。
|
|
12908
|
-
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
])
|
|
13094
|
+
// ⚠️ 不变式:这里预热的 URL 必须与上面建元素用的 URL **逐字符相同**(同一个变量,不再各写一遍字面量)——
|
|
13095
|
+
// 一旦"预热 A、播放 B",点按就会退回异步解码路径,手感立刻变钝(0.3.3 那次"变钝"的根因)。
|
|
13096
|
+
dshwvWarm([pu.empty ? '' : pu.url, ru.empty ? '' : ru.url])
|
|
12912
13097
|
} catch (err) {}
|
|
12913
13098
|
}
|
|
12914
13099
|
function playPress() {
|
|
@@ -14431,6 +14616,12 @@ function hideAudioCrop() {
|
|
|
14431
14616
|
audioCropBuffer = null
|
|
14432
14617
|
audioCropTarget = null
|
|
14433
14618
|
audioCropFileBase = ''
|
|
14619
|
+
// v753(issue #135):裁剪面板自己的 AudioContext 关闭面板就销毁 —— 它不复用,
|
|
14620
|
+
// 留着同样是"一条一直开着的系统音频流"(重新打开面板时导入音频会重新创建,见上面的 if (!audioCropCtx))。
|
|
14621
|
+
try {
|
|
14622
|
+
if (audioCropCtx) audioCropCtx.close()
|
|
14623
|
+
} catch (err) {}
|
|
14624
|
+
audioCropCtx = null
|
|
14434
14625
|
try { audioCropName.value = '' } catch (err) {}
|
|
14435
14626
|
updateAudioCropOkState()
|
|
14436
14627
|
}
|
|
@@ -14873,13 +15064,61 @@ document.addEventListener('touchstart', onDocTouchStart, { capture: true, passiv
|
|
|
14873
15064
|
|
|
14874
15065
|
var widgetCursor = ''
|
|
14875
15066
|
function setWidgetCursor(v) {
|
|
14876
|
-
if (v
|
|
14877
|
-
|
|
14878
|
-
|
|
14879
|
-
|
|
15067
|
+
if (v === widgetCursor) return
|
|
15068
|
+
widgetCursor = v
|
|
15069
|
+
// v751(PR #119):光标不写 document.body.style.cursor —— cursor 是可继承属性,写 <body> 会让 Blink
|
|
15070
|
+
// 失效整棵文档树的样式;而本函数在点击链路上按下/抬手各写一次,紧接着 isWhaleHit() 的
|
|
15071
|
+
// getBoundingClientRect() 与泡泡行测量的 getComputedStyle()/scrollWidth 会强制刷新样式+布局 ——
|
|
15072
|
+
// 整页重算就被算进了点击里。实测(见 PR #119):5.7k 节点会话约 35ms/次、13.4k 节点约 179ms/次,
|
|
15073
|
+
// 而同样一次写入只落在挂件自己子树上约 1.3ms。所以改成切换挂件自己的类,光标由 CSS 承担。
|
|
15074
|
+
// 两个类必须互斥切换(只 add 不摘旧类会让光标一直停在 grabbing)。
|
|
15075
|
+
try {
|
|
15076
|
+
root.classList.toggle('dshwv-cursor-grab', v === 'grab')
|
|
15077
|
+
root.classList.toggle('dshwv-cursor-grabbing', v === 'grabbing')
|
|
15078
|
+
} catch (err) {}
|
|
15079
|
+
}
|
|
15080
|
+
// 鲸鱼图接过指针期间(见上面的 dshwv-cursor-* 类),滚轮要转交给「指针下方真正可滚动的容器」,
|
|
15081
|
+
// 否则桌宠会变成一块滚不动的实心区域。只做一次临时让位取元素,全程只写挂件自己的内联样式,
|
|
15082
|
+
// 不碰页面级样式;找不到可滚动祖先时什么都不做(此时浏览器仍按默认把滚动交给页面滚动容器)。
|
|
15083
|
+
function onWhaleWheel(e) {
|
|
15084
|
+
try {
|
|
15085
|
+
if (!widgetCursor || !e || !e.target) return
|
|
15086
|
+
var hit = e.target
|
|
15087
|
+
if (hit !== img && !(root && root.contains && root.contains(hit))) return
|
|
15088
|
+
var prev = hit.style ? hit.style.pointerEvents : ''
|
|
15089
|
+
var under = null
|
|
15090
|
+
try {
|
|
15091
|
+
if (hit.style) hit.style.pointerEvents = 'none' // 临时让开,取出指针下方真正的页面元素
|
|
15092
|
+
under = document.elementFromPoint(e.clientX, e.clientY)
|
|
15093
|
+
} catch (err) {}
|
|
15094
|
+
try { if (hit.style) hit.style.pointerEvents = prev } catch (err) {}
|
|
15095
|
+
var sc = under
|
|
15096
|
+
for (var hop = 0; sc && sc !== document.body && sc !== document.documentElement && hop < 12; hop++) {
|
|
15097
|
+
var st = null
|
|
15098
|
+
try { st = window.getComputedStyle(sc) } catch (err) {}
|
|
15099
|
+
if (st && (st.overflowY === 'auto' || st.overflowY === 'scroll' || st.overflowY === 'overlay') &&
|
|
15100
|
+
sc.scrollHeight > sc.clientHeight + 1) break
|
|
15101
|
+
sc = sc.parentElement
|
|
15102
|
+
}
|
|
15103
|
+
if (sc && sc !== document.body && sc !== document.documentElement) {
|
|
15104
|
+
var k = (e.deltaMode === 1) ? 16 : 1 // 1 = 行模式,按 16px 折算
|
|
15105
|
+
if (e.deltaY) sc.scrollTop += e.deltaY * k
|
|
15106
|
+
if (e.deltaX) sc.scrollLeft += e.deltaX * k
|
|
15107
|
+
e.preventDefault()
|
|
15108
|
+
}
|
|
15109
|
+
} catch (err) {}
|
|
14880
15110
|
}
|
|
15111
|
+
try { root.addEventListener('wheel', onWhaleWheel, { passive: false }) } catch (err) {}
|
|
14881
15112
|
function onDocPointerMoveCursor(e) {
|
|
14882
|
-
if (drag && drag.active) {
|
|
15113
|
+
if (drag && drag.active) {
|
|
15114
|
+
// v751(PR #119):按钮已经松开却还在"拖动中" = 这一次 pointerup 丢了(例如松手时指针在窗口外):
|
|
15115
|
+
// 不能继续强推 grabbing(光标会一直卡在"抓紧"),顺手补一次 endDrag 收尾,避免挂件继续黏着鼠标。
|
|
15116
|
+
// 鼠标/触摸/笔拖动期间 e.buttons 都是 1,buttons===0 只可能意味着真的松手了。
|
|
15117
|
+
// endDrag 内部还有 drag.moved 判定,所以这里传 clickAllowed=true 不会误触发点按。
|
|
15118
|
+
if (!e.buttons) { try { endDrag(e, true) } catch (err) {} ; return }
|
|
15119
|
+
setWidgetCursor('grabbing')
|
|
15120
|
+
return
|
|
15121
|
+
}
|
|
14883
15122
|
var el = null
|
|
14884
15123
|
try { el = document.elementFromPoint(e.clientX, e.clientY) } catch (err) {}
|
|
14885
15124
|
if (el && el.closest && (el.closest('.dshwv-pop') || el.closest('.dshwv-menu') || el.closest('.dshwv-menu-btn') || el.closest('.dshwv-rolelist') || el.closest('.dshwv-cropmask') || el.closest('.dshwv-confirmmask') || el.closest('.dshwv-audiolist') || el.closest('.dshwv-audiomask') || el.closest('.dshwv-snapmask') || el.closest('.dshwv-bubmask') || el.closest('.dshwv-qedit') || el.closest('.dshwv-usagepanel') || el.closest('.dshwv-usage-mask') || el.closest('.dshwv-resmask') || el.closest('.dshwv-custmenu') || el.closest('.dshwv-custbtn'))) {
|
|
@@ -15042,7 +15281,8 @@ fetch(SIZE_URL, { cache: 'no-store' })
|
|
|
15042
15281
|
}
|
|
15043
15282
|
if (d && typeof d.vol === 'number') {
|
|
15044
15283
|
soundVol = d.vol
|
|
15045
|
-
|
|
15284
|
+
// v753(issue #135):旧配置没有 sound 字段 → 沿用"音量>0 即开"的老语义;有新字段就以它为准
|
|
15285
|
+
if (typeof d.sound !== 'boolean') soundOn = soundVol > 0
|
|
15046
15286
|
volInput.value = String(soundVol)
|
|
15047
15287
|
volPct.textContent = Math.round(soundVol * 100) + '%'
|
|
15048
15288
|
try {
|
|
@@ -15050,6 +15290,11 @@ fetch(SIZE_URL, { cache: 'no-store' })
|
|
|
15050
15290
|
if (releaseAudio) releaseAudio.volume = soundVol
|
|
15051
15291
|
} catch (err) {}
|
|
15052
15292
|
}
|
|
15293
|
+
// v753:音效总开关(configPayload 一直在写 sound 字段,但以前加载时被"音量>0"覆盖掉了 → 白存)
|
|
15294
|
+
if (d && typeof d.sound === 'boolean') {
|
|
15295
|
+
soundOn = d.sound
|
|
15296
|
+
try { soundToggle.checked = soundOn } catch (err) {}
|
|
15297
|
+
}
|
|
15053
15298
|
if (d && typeof d.soundSet === 'string' && d.soundSet) {
|
|
15054
15299
|
// 支持预设组(duck/fx1)和自定义组 id;loadAudio() 会校验自定义组是否存在
|
|
15055
15300
|
soundSet = d.soundSet
|
package/lib/index.js
CHANGED
|
@@ -985,7 +985,7 @@ export default {
|
|
|
985
985
|
const days = new Set([today, ...Object.keys(led.history || {}), ...accountingDays(led)])
|
|
986
986
|
for (const e of events) if (/^\d{4}-\d{2}-\d{2}$/.test(e.day)) days.add(e.day)
|
|
987
987
|
return {
|
|
988
|
-
ok: true, version: '0.3.
|
|
988
|
+
ok: true, version: '0.3.10', today: todayData, days7,
|
|
989
989
|
total7: total7ByCurrency[todayData.currency] || 0, total7Currency: todayData.currency, total7ByCurrency,
|
|
990
990
|
all: {
|
|
991
991
|
days: Array.from(days).filter(d => /^\d{4}-\d{2}-\d{2}$/.test(d)).sort().reverse().map(forDay),
|
|
@@ -2185,7 +2185,7 @@ export default {
|
|
|
2185
2185
|
const summary = daySummary(led, todayKey())
|
|
2186
2186
|
const nowSec = Math.floor(Date.now() / 1000)
|
|
2187
2187
|
return {
|
|
2188
|
-
...visible, version: '0.3.
|
|
2188
|
+
...visible, version: '0.3.10', isPeak: isPeakTime(nowSec),
|
|
2189
2189
|
// 峰谷切换点与节假日清单:前端倒计时要与宿主同源(否则法定节假日会算错切换点)
|
|
2190
2190
|
peakNextChangeAt: nextPeakChangeAt(nowSec),
|
|
2191
2191
|
peakHolidays: HOLIDAY_VALLEY_LIST,
|
|
@@ -3167,7 +3167,9 @@ export default {
|
|
|
3167
3167
|
function serveSound(req, res, candidates) {
|
|
3168
3168
|
const bytes = loadSound(candidates)
|
|
3169
3169
|
if (!bytes) {
|
|
3170
|
-
|
|
3170
|
+
// v752:404 也带 no-store。原来只有 200 分支有缓存头,失败的响应可能被浏览器/中间层
|
|
3171
|
+
// 缓存住,一旦缓存就会变成"永久没声音、刷新也没用"。
|
|
3172
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
3171
3173
|
res.end('sound unavailable')
|
|
3172
3174
|
return
|
|
3173
3175
|
}
|
|
@@ -3188,7 +3190,7 @@ export default {
|
|
|
3188
3190
|
const setName = soundSetFromUrl(req.url)
|
|
3189
3191
|
if (setName && !SOUND_SETS[setName]) {
|
|
3190
3192
|
const fragId = groupFragmentId(setName, 'press')
|
|
3191
|
-
if (fragId === '') { res.writeHead(204); res.end(); return } //
|
|
3193
|
+
if (fragId === '') { res.writeHead(204, { 'Cache-Control': 'no-store' }); res.end(); return } // 留空槽:该事件静音(v752:补 no-store,空响应不允许被缓存)
|
|
3192
3194
|
const bytes = loadAudioFragmentBytes(fragId)
|
|
3193
3195
|
if (bytes) {
|
|
3194
3196
|
res.writeHead(200, {
|
|
@@ -3212,7 +3214,7 @@ export default {
|
|
|
3212
3214
|
const setName = soundSetFromUrl(req.url)
|
|
3213
3215
|
if (setName && !SOUND_SETS[setName]) {
|
|
3214
3216
|
const fragId = groupFragmentId(setName, 'release')
|
|
3215
|
-
if (fragId === '') { res.writeHead(204); res.end(); return } //
|
|
3217
|
+
if (fragId === '') { res.writeHead(204, { 'Cache-Control': 'no-store' }); res.end(); return } // 留空槽:该事件静音(v752:补 no-store,空响应不允许被缓存)
|
|
3216
3218
|
const bytes = loadAudioFragmentBytes(fragId)
|
|
3217
3219
|
if (bytes) {
|
|
3218
3220
|
res.writeHead(200, {
|
package/package.json
CHANGED