zsbqb-static 0.1.0 → 0.1.2

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.
@@ -3,7 +3,35 @@
3
3
 
4
4
  const HOST_SELECTOR = "[data-burst-preview-host]";
5
5
  const instances = new Map();
6
- let observer = null;
6
+ const previewPayloadCache = new Map();
7
+
8
+ function jsonScriptText(value) {
9
+ return JSON.stringify(value)
10
+ .replace(/</g, "\\u003C")
11
+ .replace(/>/g, "\\u003E")
12
+ .replace(/&/g, "\\u0026");
13
+ }
14
+
15
+ function loadSongPreviewPayload(apiUrl) {
16
+ const cached = previewPayloadCache.get(apiUrl);
17
+ if (cached) return cached;
18
+ const boot = window.__NOTE_PREVIEW_PAYLOAD__;
19
+ const bootUrl = window.__NOTE_PREVIEW_API_URL__ || "";
20
+ const useBoot = Boolean(boot) && (!bootUrl || bootUrl === apiUrl);
21
+ if (useBoot) window.__NOTE_PREVIEW_PAYLOAD__ = null;
22
+ const pending = (useBoot
23
+ ? Promise.resolve(boot)
24
+ : fetch(apiUrl, { credentials: "same-origin" }).then(function (res) {
25
+ if (!res.ok) throw new Error("fetch_failed");
26
+ return res.json();
27
+ }))
28
+ .catch(function (error) {
29
+ previewPayloadCache.delete(apiUrl);
30
+ throw error;
31
+ });
32
+ previewPayloadCache.set(apiUrl, pending);
33
+ return pending;
34
+ }
7
35
 
8
36
  function setStatus(host, message, state) {
9
37
  const status = host.querySelector("[data-burst-preview-status]");
@@ -21,70 +49,99 @@
21
49
  return !item || item.classList.contains("is-open");
22
50
  }
23
51
 
24
- function previewLibrary() {
25
- if (window.NotePreview && typeof window.NotePreview.mount === "function") {
26
- return window.NotePreview;
27
- }
28
- const ck = window.CkHua;
29
- if (ck && typeof ck.gua_shang === "function") {
30
- return {
31
- mount: function (el, opts) {
32
- return englishPlayer(ck.gua_shang(el, opts));
33
- },
34
- preloadAssets: ck.yuJiaZai,
35
- };
36
- }
37
- return null;
52
+ function previewApiUrl() {
53
+ if (window.__NOTE_PREVIEW_API_URL__) return window.__NOTE_PREVIEW_API_URL__;
54
+ const pathParts = window.location.pathname.split("/").filter(Boolean);
55
+ if (pathParts.length < 4 || pathParts[0] !== "query") return "";
56
+ const family = pathParts[1];
57
+ const star = pathParts[2];
58
+ const title = decodeURIComponent(pathParts.slice(3).join("/"));
59
+ const mode = new URLSearchParams(window.location.search).get("mode") || "single";
60
+ const params = new URLSearchParams();
61
+ params.set("mode", mode);
62
+ return (
63
+ "/api/query/songs/" +
64
+ encodeURIComponent(family) +
65
+ "/" +
66
+ encodeURIComponent(star) +
67
+ "/" +
68
+ encodeURIComponent(title) +
69
+ "/?" +
70
+ params.toString()
71
+ );
38
72
  }
39
73
 
40
- function bindFn(raw, english, chinese) {
41
- if (!raw) return undefined;
42
- if (typeof raw[english] === "function") return raw[english].bind(raw);
43
- if (typeof raw[chinese] === "function") return raw[chinese].bind(raw);
44
- return undefined;
74
+ function blobFromPayload(host, result) {
75
+ const mode = new URLSearchParams(window.location.search).get("mode") || "single";
76
+ const kind = host.getAttribute("data-burst-clip") === "2" ? "2" : "1";
77
+ const side = mode === "double" ? "doubles" : "singles";
78
+ const style = host.getAttribute("data-style") || "";
79
+ const rank = host.getAttribute("data-rank") || "";
80
+ const slots = (result && result.song && result.song[side]) || [];
81
+ const targetSlot = slots.find(function (s) {
82
+ return s.style === style && s.rank === rank;
83
+ }) || slots[0];
84
+ if (!targetSlot) return "";
85
+ return kind === "2" ? targetSlot.preview2 || targetSlot.preview || "" : targetSlot.preview || "";
45
86
  }
46
87
 
47
- function englishPlayer(raw) {
48
- if (!raw) return raw;
49
- if (typeof raw.getTime === "function" && typeof raw.play === "function") return raw;
50
- return {
51
- seek: bindFn(raw, "seek", "laJinTiao"),
52
- play: bindFn(raw, "play", "kai_bo"),
53
- pause: bindFn(raw, "pause", "zanTingg"),
54
- isPlaying: bindFn(raw, "isPlaying", "zaiBoMe"),
55
- getTime: bindFn(raw, "getTime", "kanShiJian"),
56
- getOutput: bindFn(raw, "getOutput", "duYinLiang"),
57
- setOutput: bindFn(raw, "setOutput", "xieYinLiang"),
58
- getHitsounds: bindFn(raw, "getHitsounds", "duAnJianYin"),
59
- setHitsounds: bindFn(raw, "setHitsounds", "xieAnJianYin"),
60
- getRate: bindFn(raw, "getRate", "duBeiSu"),
61
- setRate: bindFn(raw, "setRate", "xieBeiSu"),
62
- getNoteSpeed: bindFn(raw, "getNoteSpeed", "duLiuSu"),
63
- setNoteSpeed: bindFn(raw, "setNoteSpeed", "xieLiuSu"),
64
- getBurstColor: bindFn(raw, "getBurstColor", "duBaoSe"),
65
- setBurstColor: bindFn(raw, "setBurstColor", "xieBaoSe"),
66
- on: bindFn(raw, "on", "ting_zhe"),
67
- destroy: bindFn(raw, "destroy", "chai_diao"),
68
- getSize: bindFn(raw, "getSize", "kanDaXiao"),
69
- debugIdol: bindFn(raw, "debugIdol", "kanTiaoShi"),
70
- };
88
+ function injectBlob(host, blob) {
89
+ if (!blob) return false;
90
+ if (host.querySelector('script[type="application/json"]')) return true;
91
+ const player = host.querySelector(".burst-preview__player");
92
+ if (!player) return false;
93
+ const scriptEl = document.createElement("script");
94
+ scriptEl.type = "application/json";
95
+ scriptEl.textContent = jsonScriptText(blob);
96
+ player.appendChild(scriptEl);
97
+ return true;
98
+ }
99
+
100
+ function applyPayloadToHosts(result) {
101
+ if (!result || !result.ok || !result.song) return false;
102
+ let injected = 0;
103
+ document.querySelectorAll(HOST_SELECTOR).forEach(function (host) {
104
+ const blob = blobFromPayload(host, result);
105
+ if (blob && injectBlob(host, blob)) injected += 1;
106
+ });
107
+ return injected > 0;
108
+ }
109
+
110
+ function prefetchAllPreviewPayloads() {
111
+ const apiUrl = previewApiUrl();
112
+ if (!apiUrl) return;
113
+ loadSongPreviewPayload(apiUrl)
114
+ .then(function (result) {
115
+ applyPayloadToHosts(result);
116
+ mountExpanded(document);
117
+ })
118
+ .catch(function () {
119
+ // 进页预取失败不打断爆点展示;展开时再报错。
120
+ });
71
121
  }
72
122
 
73
123
  function waitForPlayer(done) {
74
- if (previewLibrary()) {
124
+ if (window.CkHua && typeof window.CkHua.gua_shang === "function") {
75
125
  done(true);
76
126
  return;
77
127
  }
128
+ var finished = false;
129
+ var finish = function (ok) {
130
+ if (finished) return;
131
+ finished = true;
132
+ window.removeEventListener("ckhua:ok", onReady);
133
+ window.clearInterval(timer);
134
+ done(ok);
135
+ };
136
+ var onReady = function () {
137
+ if (window.CkHua && typeof window.CkHua.gua_shang === "function") finish(true);
138
+ };
139
+ window.addEventListener("ckhua:ok", onReady);
78
140
  var tries = 0;
79
141
  var timer = window.setInterval(function () {
80
142
  tries += 1;
81
- if (previewLibrary()) {
82
- window.clearInterval(timer);
83
- done(true);
84
- } else if (tries >= 400) {
85
- window.clearInterval(timer);
86
- done(false);
87
- }
143
+ if (window.CkHua && typeof window.CkHua.gua_shang === "function") finish(true);
144
+ else if (tries >= 80) finish(false);
88
145
  }, 50);
89
146
  }
90
147
 
@@ -167,28 +224,31 @@
167
224
  function applyAudioPrefs(prefs) {
168
225
  if (!prefs) return;
169
226
  const muted = effectiveMuted(prefs);
227
+ const burstColor = burstColorOf(prefs);
170
228
  instances.forEach(function (instance) {
171
- if (prefs.apply !== false && instance && typeof instance.setOutput === "function") {
172
- instance.setOutput(prefs.volume, muted);
229
+ if (prefs.apply !== false && instance && typeof instance.xieYinLiang === "function") {
230
+ instance.xieYinLiang(prefs.volume, muted);
173
231
  }
174
- if (instance && typeof instance.setHitsounds === "function") {
175
- instance.setHitsounds(Boolean(prefs.hitsounds));
232
+ if (instance && typeof instance.xieAnJianYin === "function") {
233
+ instance.xieAnJianYin(Boolean(prefs.hitsounds));
234
+ }
235
+ if (instance && typeof instance.xieBaoSe === "function") {
236
+ instance.xieBaoSe(burstColor);
176
237
  }
177
238
  });
178
- // 爆气色只影响之后挂载的播放器,避免切换偏好时立刻下载另一套贴图。
179
239
  }
180
240
 
181
241
  function bindInstancePersist(host, instance) {
182
- if (instance && typeof instance.on === "function") {
242
+ if (instance && typeof instance.ting_zhe === "function") {
183
243
  try {
184
- instance.on("volume", persistAudioPrefs);
244
+ instance.ting_zhe("volume", persistAudioPrefs);
185
245
  } catch (_error) {
186
246
  // Older players only support on("zone").
187
247
  }
188
248
  }
189
249
  const persistFromPlayer = function () {
190
- if (instance && typeof instance.getOutput === "function") {
191
- persistAudioPrefs(instance.getOutput());
250
+ if (instance && typeof instance.duYinLiang === "function") {
251
+ persistAudioPrefs(instance.duYinLiang());
192
252
  }
193
253
  };
194
254
  host.addEventListener("click", function (event) {
@@ -256,6 +316,68 @@
256
316
  return url + (url.indexOf("?") >= 0 ? "&" : "?") + "v=" + encodeURIComponent(ver);
257
317
  }
258
318
 
319
+ const warmedBgm = new Map();
320
+
321
+ function atlasPackName() {
322
+ const raw = String((window.__NOTE_PREVIEW_ATLAS_PACK__ || "")).trim().toLowerCase();
323
+ if (raw === "pinball" || raw === "bubble" || raw === "crescent") return raw;
324
+ return "idol";
325
+ }
326
+
327
+ function warmAudioUrl(url) {
328
+ url = (url || "").trim();
329
+ if (!url || warmedBgm.has(url) || typeof Audio === "undefined") return;
330
+ const audio = new Audio();
331
+ audio.autoplay = false;
332
+ audio.preload = "auto";
333
+ if (/^https?:/i.test(url)) audio.crossOrigin = "anonymous";
334
+ try {
335
+ audio.src = url;
336
+ } catch (_error) {
337
+ return;
338
+ }
339
+ try {
340
+ audio.load();
341
+ } catch (_error) {
342
+ // ignore
343
+ }
344
+ warmedBgm.set(url, audio);
345
+ }
346
+
347
+ function warmExpandedBgm() {
348
+ document.querySelectorAll(".burst-item.is-open " + HOST_SELECTOR).forEach(function (host) {
349
+ const urls = audioUrlsFor(host);
350
+ if (urls.audioUrl) warmAudioUrl(urls.audioUrl);
351
+ });
352
+ }
353
+
354
+ function preloadPlayerAssets() {
355
+ waitForPlayer(function (ready) {
356
+ if (!ready || !window.CkHua || typeof window.CkHua.yuJiaZai !== "function") {
357
+ return;
358
+ }
359
+ const prefs = readAudioPrefs();
360
+ window.CkHua.yuJiaZai({
361
+ assetBase: assetBaseFromPlayer(),
362
+ mode: atlasPackName(),
363
+ burstColor: burstColorOf(prefs),
364
+ });
365
+ });
366
+ }
367
+
368
+ function releaseWarmedBgm() {
369
+ warmedBgm.forEach(function (audio) {
370
+ try {
371
+ audio.pause();
372
+ audio.removeAttribute("src");
373
+ audio.load();
374
+ } catch (_error) {
375
+ // ignore
376
+ }
377
+ });
378
+ warmedBgm.clear();
379
+ }
380
+
259
381
  function hookPreviewAssetSrc(proto) {
260
382
  if (!proto) return;
261
383
  const desc = Object.getOwnPropertyDescriptor(proto, "src");
@@ -277,39 +399,34 @@
277
399
  hookPreviewAssetSrc(window.HTMLMediaElement && window.HTMLMediaElement.prototype);
278
400
  }
279
401
 
280
- function songFromPayload(payload) {
281
- if (!payload) return null;
282
- return payload.song || payload;
283
- }
402
+ let mountQueue = Promise.resolve();
284
403
 
285
- function blobFromSong(song, host) {
286
- const item = host.closest(".burst-item");
287
- const kind = item && item.getAttribute("data-burst-kind");
288
- const slots = kind === "double" ? (song && song.doubles) || [] : (song && song.singles) || [];
289
- const style = host.getAttribute("data-style") || "";
290
- const rank = host.getAttribute("data-rank") || "";
291
- const clip = host.getAttribute("data-burst-clip") || "1";
292
- for (let i = 0; i < slots.length; i += 1) {
293
- const slot = slots[i] || {};
294
- if (String(slot.style || "") !== style) continue;
295
- if (String(slot.rank || "") !== rank) continue;
296
- const blob = clip === "2" ? slot.preview2 : slot.preview;
297
- if (typeof blob === "string" && blob) return blob;
298
- }
299
- return "";
404
+ function anyInstancePlaying() {
405
+ let playing = false;
406
+ instances.forEach(function (instance) {
407
+ if (instance && typeof instance.zaiBoMe === "function" && instance.zaiBoMe()) {
408
+ playing = true;
409
+ }
410
+ });
411
+ return playing;
300
412
  }
301
413
 
302
- function writeSnippet(host, blob) {
303
- const player = host.querySelector(".burst-preview__player") || host;
304
- let script = host.querySelector('script[type="application/json"]');
305
- if (!script) {
306
- script = document.createElement("script");
307
- script.type = "application/json";
308
- const status = host.querySelector("[data-burst-preview-status]");
309
- player.insertBefore(script, status || null);
414
+ function runWhenPlayerIdle(fn) {
415
+ if (!anyInstancePlaying()) {
416
+ fn();
417
+ return;
310
418
  }
311
- script.textContent = JSON.stringify(blob);
312
- return script;
419
+ mountQueue = mountQueue.then(function () {
420
+ return new Promise(function (resolve) {
421
+ window.requestAnimationFrame(function () {
422
+ try {
423
+ fn();
424
+ } finally {
425
+ resolve();
426
+ }
427
+ });
428
+ });
429
+ });
313
430
  }
314
431
 
315
432
  function mount(host) {
@@ -318,153 +435,182 @@
318
435
  }
319
436
 
320
437
  const canvas = host.querySelector("[data-burst-preview-canvas]");
321
- if (!canvas) {
322
- setStatus(host, "谱面预览数据缺失。", "error");
323
- return;
324
- }
325
438
  let data = host.querySelector('script[type="application/json"]');
326
- if (!data || !String(data.textContent || "").trim()) {
327
- const pending = window.__NOTE_PREVIEW_PAYLOAD__;
328
- if (pending && typeof pending.then === "function") {
329
- host.dataset.previewMounting = "1";
330
- setStatus(host, "谱面预览加载中…", "loading");
331
- Promise.resolve(pending)
332
- .then(function (payload) {
333
- const blob = blobFromSong(songFromPayload(payload), host);
334
- if (!blob) throw new Error("preview blob missing");
335
- writeSnippet(host, blob);
336
- delete host.dataset.previewMounting;
337
- mount(host);
338
- })
339
- .catch(function (error) {
340
- delete host.dataset.previewMounting;
341
- if (typeof console !== "undefined" && console.error) {
342
- console.error("谱面预览加载失败", error);
343
- }
344
- setStatus(host, "谱面预览数据缺失。", "error");
345
- });
439
+ if (!canvas) return;
440
+
441
+ if (!data) {
442
+ if (host.dataset.previewFetching === "1") return;
443
+ host.dataset.previewFetching = "1";
444
+ setStatus(host, "正在加载谱面…", "loading");
445
+
446
+ const apiUrl = previewApiUrl();
447
+ if (!apiUrl) {
448
+ host.dataset.previewFetching = "";
449
+ setStatus(host, "谱面预览数据缺失。", "error");
346
450
  return;
347
451
  }
348
- setStatus(host, "谱面预览数据缺失。", "error");
452
+
453
+ loadSongPreviewPayload(apiUrl)
454
+ .then(function (result) {
455
+ host.dataset.previewFetching = "";
456
+ if (!result || !result.ok || !result.song) {
457
+ setStatus(host, "谱面数据加载失败。", "error");
458
+ return;
459
+ }
460
+ applyPayloadToHosts(result);
461
+ if (!blobFromPayload(host, result)) {
462
+ setStatus(host, "该档位无谱面预览。", "ready");
463
+ return;
464
+ }
465
+ if (isExpanded(host)) mount(host);
466
+ })
467
+ .catch(function () {
468
+ host.dataset.previewFetching = "";
469
+ setStatus(host, "谱面获取失败,请重试。", "error");
470
+ });
349
471
  return;
350
472
  }
351
473
 
352
474
  host.dataset.previewMounting = "1";
353
475
  setStatus(host, "谱面预览加载中…", "loading");
354
476
  waitForPlayer(function (ready) {
355
- try {
356
- const lib = previewLibrary();
357
- if (!ready || !lib) throw new Error("preview player unavailable");
358
- const snippet = JSON.parse(data.textContent);
359
- if (typeof snippet !== "string" || !snippet) {
360
- throw new Error("invalid preview snippet");
361
- }
362
- const startComboRaw = host.getAttribute("data-start-combo");
363
- const startCombo = startComboRaw ? Number(startComboRaw) : NaN;
364
- const prefs = readAudioPrefs();
365
- const muted = effectiveMuted(prefs);
366
- const audio = audioUrlsFor(host);
367
- const instance = lib.mount(canvas, {
368
- snippet: snippet,
369
- assetBase: assetBaseFromPlayer(),
370
- audioUrl: audio.audioUrl || undefined,
371
- audioFallbackUrl: audio.audioFallbackUrl || undefined,
372
- startCombo: Number.isFinite(startCombo) ? startCombo : undefined,
373
- hitsounds: Boolean(prefs.hitsounds),
374
- burstColor: burstColorOf(prefs),
375
- burstSfx: true,
376
- volume: prefs.volume,
377
- muted: muted,
378
- });
379
- if (instance && typeof instance.setOutput === "function") {
380
- instance.setOutput(prefs.volume, muted);
381
- }
382
- if (instance && typeof instance.setHitsounds === "function") {
383
- instance.setHitsounds(Boolean(prefs.hitsounds));
384
- }
385
- if (instance && typeof instance.setBurstColor === "function") {
386
- instance.setBurstColor(burstColorOf(prefs));
387
- }
388
- bindInstancePersist(host, instance);
389
- instances.set(host, instance || null);
390
- host.__notePreview = instance || null;
391
- window.__notePreview = instance || null;
392
- setStatus(host, "", "ready");
393
- } catch (error) {
394
- if (typeof console !== "undefined" && console.error) {
395
- console.error("谱面预览加载失败", error);
477
+ runWhenPlayerIdle(function () {
478
+ try {
479
+ if (!host.isConnected || instances.has(host) || host.dataset.previewMounting !== "1") {
480
+ return;
481
+ }
482
+ if (!isExpanded(host)) {
483
+ return;
484
+ }
485
+ if (!ready) throw new Error("preview player unavailable");
486
+ const snippet = JSON.parse(data.textContent);
487
+ if (typeof snippet !== "string" || !snippet) {
488
+ throw new Error("invalid preview snippet");
489
+ }
490
+ const startComboRaw = host.getAttribute("data-start-combo");
491
+ const startCombo = startComboRaw ? Number(startComboRaw) : NaN;
492
+ const prefs = readAudioPrefs();
493
+ const muted = effectiveMuted(prefs);
494
+ const audio = audioUrlsFor(host);
495
+ const leadRaw = Number(host.getAttribute("data-audio-lead-ms") || 0);
496
+ const instance = window.CkHua.gua_shang(canvas, {
497
+ snippet: snippet,
498
+ assetBase: assetBaseFromPlayer(),
499
+ audioUrl: audio.audioUrl || undefined,
500
+ audioFallbackUrl: audio.audioFallbackUrl || undefined,
501
+ audioLeadMs: Number.isFinite(leadRaw) ? leadRaw : 0,
502
+ startCombo: Number.isFinite(startCombo) ? startCombo : undefined,
503
+ hitsounds: Boolean(prefs.hitsounds),
504
+ burstColor: burstColorOf(prefs),
505
+ burstSfx: true,
506
+ volume: prefs.volume,
507
+ muted: muted,
508
+ });
509
+ if (instance && typeof instance.xieYinLiang === "function") {
510
+ instance.xieYinLiang(prefs.volume, muted);
511
+ }
512
+ if (instance && typeof instance.xieAnJianYin === "function") {
513
+ instance.xieAnJianYin(Boolean(prefs.hitsounds));
514
+ }
515
+ if (instance && typeof instance.xieBaoSe === "function") {
516
+ instance.xieBaoSe(burstColorOf(prefs));
517
+ }
518
+ bindInstancePersist(host, instance);
519
+ instances.set(host, instance || null);
520
+ host.__notePreview = instance || null;
521
+ window.__notePreview = instance || null;
522
+ setStatus(host, "", "ready");
523
+ } catch (error) {
524
+ if (typeof console !== "undefined" && console.error) {
525
+ console.error("谱面预览加载失败", error);
526
+ }
527
+ setStatus(host, "谱面预览加载失败,请稍后重试。", "error");
528
+ } finally {
529
+ delete host.dataset.previewMounting;
396
530
  }
397
- setStatus(host, "谱面预览加载失败,请稍后重试。", "error");
398
- } finally {
399
- delete host.dataset.previewMounting;
400
- }
531
+ });
401
532
  });
402
533
  }
403
534
 
535
+ function unmountHost(host) {
536
+ if (!host) return;
537
+ const instance = instances.get(host);
538
+ if (instance) {
539
+ try {
540
+ if (typeof instance.zanTingg === "function") instance.zanTingg();
541
+ } catch (_error) {
542
+ // ignore
543
+ }
544
+ try {
545
+ if (typeof instance.chai_diao === "function") instance.chai_diao();
546
+ } catch (_error) {
547
+ // ignore
548
+ }
549
+ }
550
+ instances.delete(host);
551
+ if (host.__notePreview) host.__notePreview = null;
552
+ if (window.__notePreview === instance) window.__notePreview = null;
553
+ delete host.dataset.previewMounting;
554
+ setStatus(host, "谱面预览加载中…", "loading");
555
+ }
556
+
557
+ function pauseHost(host) {
558
+ const instance = instances.get(host);
559
+ if (!instance || typeof instance.zanTingg !== "function") return;
560
+ if (typeof instance.zaiBoMe === "function" && !instance.zaiBoMe()) return;
561
+ try {
562
+ instance.zanTingg();
563
+ } catch (_error) {
564
+ // ignore
565
+ }
566
+ }
567
+
568
+ function pauseItem(item) {
569
+ if (!item) return;
570
+ item.querySelectorAll(HOST_SELECTOR).forEach(pauseHost);
571
+ }
572
+
404
573
  function mountExpanded(root) {
405
574
  const scope = root && root.querySelectorAll ? root : document;
406
- scope.querySelectorAll(HOST_SELECTOR).forEach(mount);
407
- if (scope.matches && scope.matches(HOST_SELECTOR)) mount(scope);
575
+ scope.querySelectorAll(".burst-item").forEach(syncItem);
576
+ if (scope.classList && scope.classList.contains("burst-item")) syncItem(scope);
408
577
  }
409
578
 
410
- function mountItem(item) {
411
- if (!item || !item.classList.contains("is-open")) return;
579
+ function syncItem(item) {
580
+ if (!item) return;
581
+ if (!item.classList.contains("is-open")) {
582
+ // 收起只是 CSS 隐藏卡片:暂停即可,不要 destroy/重新挂载。
583
+ pauseItem(item);
584
+ return;
585
+ }
412
586
  item.querySelectorAll(HOST_SELECTOR).forEach(mount);
587
+ warmExpandedBgm();
588
+ }
589
+
590
+ function mountItem(item) {
591
+ syncItem(item);
413
592
  }
414
593
 
415
594
  function onAccordionChange(event) {
416
595
  const item = event.target.closest ? event.target.closest(".burst-item") : null;
417
- mountItem(item);
596
+ syncItem(item);
418
597
  }
419
598
 
420
599
  function destroyAll() {
421
- if (observer) {
422
- observer.disconnect();
423
- observer = null;
424
- }
425
- instances.forEach(function (instance) {
426
- if (instance && typeof instance.destroy === "function") {
427
- try {
428
- instance.destroy();
429
- } catch (_error) {
430
- // The page is leaving; one failed teardown must not block the others.
431
- }
432
- }
433
- });
434
- instances.clear();
600
+ Array.from(instances.keys()).forEach(unmountHost);
601
+ releaseWarmedBgm();
435
602
  }
436
603
 
437
604
  function init() {
438
605
  installPreviewAssetVersioning();
606
+ prefetchAllPreviewPayloads();
607
+ preloadPlayerAssets();
608
+ warmExpandedBgm();
439
609
  mountExpanded(document);
440
610
 
441
- document.addEventListener("click", function (event) {
442
- const toggle = event.target.closest ? event.target.closest("[data-burst-toggle]") : null;
443
- if (!toggle) return;
444
- window.setTimeout(function () {
445
- mountItem(toggle.closest(".burst-item"));
446
- }, 0);
447
- });
448
611
  document.addEventListener("burst:accordion-change", onAccordionChange);
449
612
  document.addEventListener("burst-accordion:change", onAccordionChange);
450
613
 
451
- observer = new MutationObserver(function (mutations) {
452
- mutations.forEach(function (mutation) {
453
- if (
454
- mutation.type === "attributes" &&
455
- mutation.attributeName === "class" &&
456
- mutation.target.classList.contains("burst-item")
457
- ) {
458
- mountItem(mutation.target);
459
- }
460
- });
461
- });
462
- observer.observe(document.body, {
463
- subtree: true,
464
- attributes: true,
465
- attributeFilter: ["class"],
466
- });
467
-
468
614
  window.addEventListener(AUDIO_PREFS_EVENT, function (event) {
469
615
  const prefs = (event && event.detail) || readAudioPrefs();
470
616
  applyAudioPrefs(prefs);
@@ -472,15 +618,7 @@
472
618
 
473
619
  window.addEventListener("load", function () {
474
620
  mountExpanded(document);
475
- });
476
- Array.prototype.slice.call(document.querySelectorAll('script[src*="note-preview"]')).forEach(function (el) {
477
- el.addEventListener(
478
- "load",
479
- function () {
480
- mountExpanded(document);
481
- },
482
- { once: true }
483
- );
621
+ warmExpandedBgm();
484
622
  });
485
623
  window.addEventListener("pagehide", destroyAll, { once: true });
486
624
  }