zsbqb-static 0.1.0

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.
Files changed (52) hide show
  1. package/README.md +4 -0
  2. package/catalog/bubble-4f53cda18c2b.json +1 -0
  3. package/catalog/crescent-4f53cda18c2b.json +1 -0
  4. package/catalog/dianfeng-4f53cda18c2b.json +1 -0
  5. package/catalog/manifest.json +1 -0
  6. package/catalog/pinball-4f53cda18c2b.json +1 -0
  7. package/catalog/pinyin-4f53cda18c2b.json +1 -0
  8. package/catalog/xingdong-4f53cda18c2b.json +1 -0
  9. package/css/app.css +2157 -0
  10. package/img/apple-touch-icon.png +0 -0
  11. package/img/brand-logo.png +0 -0
  12. package/img/favicon-192.png +0 -0
  13. package/img/favicon-32.png +0 -0
  14. package/img/favicon.ico +0 -0
  15. package/img/modes/bubble.png +0 -0
  16. package/img/modes/crescent.png +0 -0
  17. package/img/modes/dianfeng-master.png +0 -0
  18. package/img/modes/dianfeng.png +0 -0
  19. package/img/modes/master.png +0 -0
  20. package/img/modes/pinball.png +0 -0
  21. package/img/modes/xingdong.png +0 -0
  22. package/img/skills/fire.png +0 -0
  23. package/img/skills/limit.png +0 -0
  24. package/js/app.js +2124 -0
  25. package/js/auth-shell-sync.js +45 -0
  26. package/js/burst-preview-host.js +493 -0
  27. package/js/catalog-search.js +311 -0
  28. package/js/query-app.js +331 -0
  29. package/note-preview/assets/atlas/bubble.webp +0 -0
  30. package/note-preview/assets/atlas/crescent.webp +0 -0
  31. package/note-preview/assets/atlas/idol.webp +0 -0
  32. package/note-preview/assets/atlas/manifest.json +1 -0
  33. package/note-preview/assets/atlas/pinball.webp +0 -0
  34. package/note-preview/assets/atlas/shared.webp +0 -0
  35. package/note-preview/assets/sfx/burst-end.m4a +0 -0
  36. package/note-preview/assets/sfx/burst-start.m4a +0 -0
  37. package/note-preview/assets/sfx/key-tone-bubble-slide.m4a +0 -0
  38. package/note-preview/assets/sfx/key-tone-bubble.m4a +0 -0
  39. package/note-preview/assets/sfx/key-tone-classic.m4a +0 -0
  40. package/note-preview/assets/sfx/key-tone-crescent-slide.m4a +0 -0
  41. package/note-preview/assets/sfx/key-tone-crescent.m4a +0 -0
  42. package/note-preview/assets/sfx/key-tone-long.m4a +0 -0
  43. package/note-preview/assets/sfx/key-tone-pinball-slide.m4a +0 -0
  44. package/note-preview/assets/sfx/key-tone-pinball.m4a +0 -0
  45. package/note-preview/assets/sfx/key-tone-rhythm.m4a +0 -0
  46. package/note-preview/assets/sfx/key-tone-slide.m4a +0 -0
  47. package/note-preview/assets/sfx/key-tone-xingdong-slide.m4a +0 -0
  48. package/note-preview/assets/sfx/key-tone-xingdong.m4a +0 -0
  49. package/note-preview/assets/sfx/key-tone.m4a +0 -0
  50. package/note-preview/note-preview.min.js +1 -0
  51. package/package.json +18 -0
  52. package/public/season.json +1 -0
@@ -0,0 +1,311 @@
1
+ /** 浏览器歌单搜索:与 apps/core/search_index.py 对齐(子串打分 + 模糊回落)。 */
2
+ (function (root) {
3
+ const FUZZY_MIN_RATIO = 0.72;
4
+ const FUZZY_LIMIT = 12;
5
+ const NOISE_RE = /[\s\-_/\\|·・'"“”‘’[\]()()【】<>《》,,.。!!??::;;~~]+/g;
6
+
7
+ let charPinyin = {};
8
+
9
+ function setCharPinyinMap(map) {
10
+ charPinyin = map && typeof map === "object" ? map : {};
11
+ }
12
+
13
+ function compactNoise(value) {
14
+ return String(value || "")
15
+ .toLowerCase()
16
+ .replace(NOISE_RE, "");
17
+ }
18
+
19
+ function lower(value) {
20
+ return String(value || "").toLowerCase();
21
+ }
22
+
23
+ function queryPinyin(text) {
24
+ const compact = compactNoise(text);
25
+ if (!compact) return "";
26
+ let out = "";
27
+ for (const ch of compact) {
28
+ out += charPinyin[ch] || ch;
29
+ }
30
+ return out;
31
+ }
32
+
33
+ function tokenVariants(token) {
34
+ const t = lower(token).trim();
35
+ if (!t) return [];
36
+ const compact = t.replace(NOISE_RE, "");
37
+ const variants = [t];
38
+ if (compact && compact !== t) variants.push(compact);
39
+ return variants;
40
+ }
41
+
42
+ function fieldValues(entry, keys) {
43
+ const out = [];
44
+ keys.forEach((key) => {
45
+ const raw = entry[key];
46
+ if (raw == null || raw === "") return;
47
+ const val = String(raw).toLowerCase();
48
+ out.push(val);
49
+ const compact = val.replace(NOISE_RE, "");
50
+ if (compact !== val) out.push(compact);
51
+ });
52
+ return out;
53
+ }
54
+
55
+ const SEARCHABLE_KEYS = [
56
+ "title",
57
+ "artist",
58
+ "title_compact",
59
+ "artist_compact",
60
+ "title_pinyin",
61
+ "artist_pinyin",
62
+ "title_initials",
63
+ "artist_initials",
64
+ "title_first_pinyin",
65
+ "artist_first_pinyin",
66
+ "title_first_letter",
67
+ "artist_first_letter",
68
+ ];
69
+
70
+ function tokenMatches(entry, token) {
71
+ const variants = tokenVariants(token);
72
+ if (!variants.length) return true;
73
+ const haystack = fieldValues(entry, SEARCHABLE_KEYS);
74
+ for (const variant of variants) {
75
+ for (const hay of haystack) {
76
+ if (hay.includes(variant) || hay.startsWith(variant)) return true;
77
+ }
78
+ }
79
+ return false;
80
+ }
81
+
82
+ function matchingBlocks(a, b) {
83
+ const la = a.length;
84
+ const lb = b.length;
85
+ const match = new Array(lb);
86
+ const blocks = [];
87
+ let ai = 0;
88
+ while (ai < la) {
89
+ let bestJ = -1;
90
+ let bestLen = 0;
91
+ for (let bj = 0; bj < lb; bj++) {
92
+ if (match[bj]) continue;
93
+ if (a[ai] !== b[bj]) continue;
94
+ let len = 0;
95
+ while (ai + len < la && bj + len < lb && !match[bj + len] && a[ai + len] === b[bj + len]) {
96
+ len += 1;
97
+ }
98
+ if (len > bestLen) {
99
+ bestLen = len;
100
+ bestJ = bj;
101
+ }
102
+ }
103
+ if (bestLen > 0) {
104
+ for (let k = 0; k < bestLen; k++) match[bestJ + k] = 1;
105
+ blocks.push(bestLen);
106
+ ai += bestLen;
107
+ } else {
108
+ ai += 1;
109
+ }
110
+ }
111
+ return blocks;
112
+ }
113
+
114
+ function sequenceRatio(left, right) {
115
+ if (!left && !right) return 1;
116
+ if (!left || !right) return 0;
117
+ if (left === right) return 1;
118
+ const blocks = matchingBlocks(left, right);
119
+ const matches = blocks.reduce((sum, n) => sum + n, 0);
120
+ return (2 * matches) / (left.length + right.length);
121
+ }
122
+
123
+ function similarity(left, right) {
124
+ if (!left || !right) return 0;
125
+ if (left === right) return 1;
126
+ const shorter = left.length <= right.length ? left : right;
127
+ const longer = left.length <= right.length ? right : left;
128
+ if (shorter.length >= 2 && longer.includes(shorter)) {
129
+ return Math.max(0.75, shorter.length / Math.max(longer.length, 1));
130
+ }
131
+ return sequenceRatio(left, right);
132
+ }
133
+
134
+ function fuzzyScore(entry, query) {
135
+ const needle = lower(query).trim();
136
+ if (!needle) return 0;
137
+ const compact = needle.replace(NOISE_RE, "");
138
+ if (compact.length < 2) return 0;
139
+ const needlePy = queryPinyin(needle);
140
+ const fields = [
141
+ entry.title_compact || String(entry.title || "").toLowerCase().replace(NOISE_RE, ""),
142
+ entry.artist_compact || String(entry.artist || "").toLowerCase().replace(NOISE_RE, ""),
143
+ entry.title_pinyin || "",
144
+ entry.artist_pinyin || "",
145
+ ];
146
+ if (compact.length <= 6 && /^[\x00-\x7f]+$/.test(compact)) {
147
+ fields.push(entry.title_initials || "", entry.artist_initials || "");
148
+ }
149
+ let best = 0;
150
+ fields.forEach((hay) => {
151
+ if (!hay || hay.length < 2) return;
152
+ best = Math.max(best, similarity(compact, hay));
153
+ if (needlePy && needlePy !== compact && needlePy.length >= 2) {
154
+ best = Math.max(best, similarity(needlePy, hay));
155
+ }
156
+ });
157
+ const titlePy = entry.title_pinyin || "";
158
+ if (needlePy && titlePy && needlePy.length >= 2) {
159
+ best = Math.max(best, similarity(needlePy, titlePy));
160
+ if (titlePy.startsWith(needlePy) || needlePy.startsWith(titlePy)) {
161
+ best = Math.max(best, 0.82);
162
+ }
163
+ }
164
+ if (best < FUZZY_MIN_RATIO) return 0;
165
+ return Math.round(best * 100);
166
+ }
167
+
168
+ function matchScore(entry, query) {
169
+ const needle = lower(query).trim();
170
+ if (!needle) return 0;
171
+ const tokens = needle.split(/\s+/).filter(Boolean);
172
+ if (!tokens.length) return 0;
173
+ for (const token of tokens) {
174
+ if (!tokenMatches(entry, token)) {
175
+ const compactToken = token.replace(NOISE_RE, "");
176
+ if (!compactToken || !tokenMatches(entry, compactToken)) return 0;
177
+ }
178
+ }
179
+ const needleCompact = needle.replace(NOISE_RE, "");
180
+ const title = lower(entry.title);
181
+ const artist = lower(entry.artist);
182
+ const titleCompact = entry.title_compact || title.replace(NOISE_RE, "");
183
+ const artistCompact = entry.artist_compact || artist.replace(NOISE_RE, "");
184
+ let score = 10;
185
+ if (title === needle || titleCompact === needleCompact) score += 100;
186
+ else if (title.startsWith(needle) || titleCompact.startsWith(needleCompact)) score += 60;
187
+ else if (title.includes(needle) || titleCompact.includes(needleCompact)) score += 40;
188
+ if (artist === needle || artistCompact === needleCompact) score += 50;
189
+ else if (artist.includes(needle) || artistCompact.includes(needleCompact)) score += 25;
190
+ const titlePinyin = entry.title_pinyin || "";
191
+ const artistPinyin = entry.artist_pinyin || "";
192
+ const needlePy = queryPinyin(needle);
193
+ if (
194
+ needleCompact &&
195
+ (titlePinyin.startsWith(needleCompact) ||
196
+ artistPinyin.startsWith(needleCompact) ||
197
+ titlePinyin.includes(needleCompact) ||
198
+ artistPinyin.includes(needleCompact))
199
+ ) {
200
+ score += 20;
201
+ }
202
+ if (
203
+ needlePy &&
204
+ needlePy !== needleCompact &&
205
+ (titlePinyin.startsWith(needlePy) ||
206
+ titlePinyin.includes(needlePy) ||
207
+ artistPinyin.startsWith(needlePy) ||
208
+ artistPinyin.includes(needlePy))
209
+ ) {
210
+ score += 35;
211
+ }
212
+ const titleInitials = entry.title_initials || "";
213
+ const artistInitials = entry.artist_initials || "";
214
+ if (
215
+ needleCompact &&
216
+ (titleInitials.startsWith(needleCompact) ||
217
+ artistInitials.startsWith(needleCompact) ||
218
+ titleInitials.includes(needleCompact) ||
219
+ artistInitials.includes(needleCompact))
220
+ ) {
221
+ score += 15;
222
+ }
223
+ if (tokens.length === 1 && tokens[0].length === 1) {
224
+ const token = tokens[0];
225
+ if (token === entry.title_first_letter || token === entry.artist_first_letter) score += 12;
226
+ }
227
+ return score;
228
+ }
229
+
230
+ function bpmMatches(entry, bpmBucket, bpmExact) {
231
+ const raw = entry.bpm;
232
+ const bpm = Number(raw);
233
+ const hasBpm = raw !== "" && raw != null && Number.isFinite(bpm);
234
+ if (bpmExact || bpmBucket) {
235
+ if (!hasBpm) return false;
236
+ if (bpmExact) return Math.round(bpm) === parseInt(bpmExact, 10);
237
+ if (bpmBucket === "100-") return bpm < 100;
238
+ if (bpmBucket === "200+") return bpm >= 200;
239
+ const low = parseInt(bpmBucket, 10);
240
+ return bpm >= low && bpm < low + 10;
241
+ }
242
+ return true;
243
+ }
244
+
245
+ function letterMatches(entry, letter) {
246
+ const key = String(letter || "").trim();
247
+ if (!key) return true;
248
+ if (key === "#") return Boolean(entry.title_lead_non_zh_en);
249
+ return String(entry.title_first_letter || "").toLowerCase() === key.toLowerCase();
250
+ }
251
+
252
+ function titleSortKey(title) {
253
+ const text = String(title || "");
254
+ const ch = text.trim()[0] || "";
255
+ const nonZhEn = ch && !/[A-Za-z]/.test(ch) && !/[\u4e00-\u9fff]/.test(ch);
256
+ return [nonZhEn ? 0 : 1, text];
257
+ }
258
+
259
+ function searchCatalog(songs, query, filters) {
260
+ const list = Array.isArray(songs) ? songs : [];
261
+ const needle = String(query || "").trim();
262
+ const star = String((filters && filters.star) || "").trim();
263
+ const letter = String((filters && filters.letter) || "").trim();
264
+ const bpmBucket = String((filters && filters.bpmBucket) || "").trim();
265
+ const bpmExact = (filters && filters.bpmExact) || "";
266
+ const exact = [];
267
+ list.forEach((entry) => {
268
+ if (star && String(entry.star) !== star) return;
269
+ if (!letterMatches(entry, letter)) return;
270
+ if (!bpmMatches(entry, bpmBucket, bpmExact)) return;
271
+ if (!needle) {
272
+ exact.push({ entry, score: 0, fuzzy: false });
273
+ return;
274
+ }
275
+ const score = matchScore(entry, needle);
276
+ if (score > 0) exact.push({ entry, score, fuzzy: false });
277
+ });
278
+ let ranked = exact;
279
+ if (needle && !ranked.length) {
280
+ const fuzzy = [];
281
+ list.forEach((entry) => {
282
+ if (star && String(entry.star) !== star) return;
283
+ if (!letterMatches(entry, letter)) return;
284
+ if (!bpmMatches(entry, bpmBucket, bpmExact)) return;
285
+ const score = fuzzyScore(entry, needle);
286
+ if (score > 0) fuzzy.push({ entry, score, fuzzy: true });
287
+ });
288
+ fuzzy.sort((a, b) => b.score - a.score || titleSortKey(a.entry.title).join("").localeCompare(titleSortKey(b.entry.title).join("")));
289
+ ranked = fuzzy.slice(0, FUZZY_LIMIT);
290
+ } else if (needle) {
291
+ ranked.sort((a, b) => b.score - a.score || String(a.entry.title).localeCompare(String(b.entry.title), "zh"));
292
+ } else {
293
+ ranked.sort((a, b) => {
294
+ const ka = titleSortKey(a.entry.title);
295
+ const kb = titleSortKey(b.entry.title);
296
+ if (ka[0] !== kb[0]) return ka[0] - kb[0];
297
+ return String(ka[1]).localeCompare(String(kb[1]), "zh");
298
+ });
299
+ }
300
+ return ranked;
301
+ }
302
+
303
+ root.CatalogSearch = {
304
+ setCharPinyinMap,
305
+ queryPinyin,
306
+ matchScore,
307
+ fuzzyScore,
308
+ searchCatalog,
309
+ FUZZY_LIMIT,
310
+ };
311
+ })(typeof window !== "undefined" ? window : globalThis);
@@ -0,0 +1,331 @@
1
+ /** 查询页:从缓存的 catalog JSON 渲染歌单并用 CatalogSearch 过滤。 */
2
+ (function () {
3
+ const boot = window.__QUERY_CATALOG__ || {};
4
+ const songList = document.querySelector("[data-query-app]");
5
+ if (!songList || !boot.familyUrls) return;
6
+
7
+ const emptyHint = songList.querySelector("[data-client-empty]");
8
+ const countEl = document.querySelector("[data-song-count]");
9
+ const qHint = document.querySelector("[data-query-q-hint]");
10
+ const hintTail = document.querySelector("[data-query-hint-tail]");
11
+ const searchInput = document.querySelector("[data-search-input]");
12
+ const starNative = document.querySelector(".star-picker__native");
13
+ const bpmBucketInput = document.querySelector("[data-bpm-bucket]");
14
+ const bpmExactField = document.querySelector("[data-bpm-exact-field]");
15
+ const familyNative = document.querySelector(".family-picker__native");
16
+
17
+ const cache = Object.create(null);
18
+ let songs = [];
19
+ let pinyinReady = null;
20
+ let family = boot.family || (familyNative && familyNative.value) || "xingdong";
21
+ let fuzzySuggest = false;
22
+
23
+ const urlParams = new URLSearchParams(window.location.search);
24
+ let filterState = {
25
+ q: (urlParams.get("q") || "").trim(),
26
+ star: (urlParams.get("star") || "").trim(),
27
+ letter: (urlParams.get("letter") || "").trim(),
28
+ bpmBucket: (urlParams.get("bpm") || "").trim(),
29
+ bpmExact: (urlParams.get("bpm_exact") || "").trim(),
30
+ };
31
+
32
+ if (searchInput && filterState.q && searchInput.value !== filterState.q) {
33
+ searchInput.value = filterState.q;
34
+ const clearBtn = searchInput.parentElement && searchInput.parentElement.querySelector("[data-search-clear]");
35
+ if (clearBtn) clearBtn.classList.toggle("is-on", filterState.q.length > 0);
36
+ }
37
+
38
+ function fetchCatalog(url, cacheMode) {
39
+ const abs = /^https?:/i.test(url);
40
+ const same = !abs || url.indexOf(window.location.origin) === 0;
41
+ return fetch(url, {
42
+ credentials: same ? "same-origin" : "omit",
43
+ mode: same ? "same-origin" : "cors",
44
+ cache: cacheMode,
45
+ priority: "high",
46
+ });
47
+ }
48
+
49
+ function cachedFetch(url) {
50
+ if (!url) return Promise.resolve(null);
51
+ if (cache[url]) return cache[url];
52
+ cache[url] = fetchCatalog(url, "force-cache")
53
+ .then((res) => {
54
+ if (!res.ok) throw new Error("catalog_http");
55
+ return res.json();
56
+ })
57
+ .catch(() =>
58
+ fetchCatalog(url, "reload").then((res) => {
59
+ if (!res.ok) throw new Error("catalog_http");
60
+ return res.json();
61
+ })
62
+ );
63
+ return cache[url];
64
+ }
65
+
66
+ function ensurePinyin() {
67
+ if (pinyinReady) return pinyinReady;
68
+ if (!boot.pinyinUrl || !window.CatalogSearch) {
69
+ pinyinReady = Promise.resolve();
70
+ return pinyinReady;
71
+ }
72
+ pinyinReady = cachedFetch(boot.pinyinUrl).then((map) => {
73
+ window.CatalogSearch.setCharPinyinMap(map || {});
74
+ });
75
+ return pinyinReady;
76
+ }
77
+
78
+ function loadFamily(nextFamily) {
79
+ const key = nextFamily || family;
80
+ const url = boot.familyUrls[key];
81
+ if (!url) return Promise.resolve([]);
82
+ return ensurePinyin().then(() =>
83
+ cachedFetch(url).then((payload) => {
84
+ songs = (payload && payload.songs) || [];
85
+ family = key;
86
+ syncFamilyHint((payload && payload.label) || key);
87
+ rebuildStarPicker((boot.starChoices && boot.starChoices[key]) || []);
88
+ return songs;
89
+ })
90
+ );
91
+ }
92
+
93
+ function escapeHtml(value) {
94
+ return String(value || "")
95
+ .replace(/&/g, "&amp;")
96
+ .replace(/</g, "&lt;")
97
+ .replace(/>/g, "&gt;")
98
+ .replace(/"/g, "&quot;");
99
+ }
100
+
101
+ function detailBase(row) {
102
+ return `/query/${encodeURIComponent(row.family)}/${encodeURIComponent(row.star)}/${encodeURIComponent(row.title)}/`;
103
+ }
104
+
105
+ function iconFor(row) {
106
+ return (boot.icons && boot.icons[row.family]) || "";
107
+ }
108
+
109
+ function bpmLabel(row) {
110
+ if (row.bpm == null || row.bpm === "") return "";
111
+ const n = Number(row.bpm);
112
+ return Number.isFinite(n) ? String(Math.round(n)) : "";
113
+ }
114
+
115
+ function rebuildStarPicker(stars) {
116
+ if (!starNative) return;
117
+ const allowed = (stars || []).map(String);
118
+ if (filterState.star && allowed.indexOf(String(filterState.star)) < 0) {
119
+ filterState.star = "";
120
+ }
121
+ const current = String(filterState.star || "");
122
+ starNative.innerHTML =
123
+ '<option value="">全部</option>' +
124
+ allowed
125
+ .map((star) => `<option value="${star}"${star === current ? " selected" : ""}>${star}星</option>`)
126
+ .join("");
127
+ starNative.value = current;
128
+ const menu = document.querySelector("[data-star-menu]");
129
+ if (menu) {
130
+ menu.innerHTML =
131
+ `<button type="button" class="star-picker__option${current ? "" : " is-on"}" data-star-option data-star-value="" role="option" aria-selected="${current ? "false" : "true"}">全部</button>` +
132
+ allowed
133
+ .map(
134
+ (star) =>
135
+ `<button type="button" class="star-picker__option${star === current ? " is-on" : ""}" data-star-option data-star-value="${star}" role="option" aria-selected="${star === current ? "true" : "false"}">${star}星</button>`
136
+ )
137
+ .join("");
138
+ }
139
+ const valueEl = document.querySelector("[data-star-value]");
140
+ if (valueEl) valueEl.textContent = current ? `${current}星` : "全部";
141
+ const hintStar = document.querySelector(".query-hint__star");
142
+ if (hintStar) {
143
+ hintStar.textContent = current ? `${current}星` : "";
144
+ hintStar.hidden = !current;
145
+ }
146
+ }
147
+
148
+ function syncFamilyHint(label) {
149
+ const hint = document.querySelector(".query-hint__mode");
150
+ if (!hint) return;
151
+ hint.setAttribute("data-family", family);
152
+ hint.textContent = label || family;
153
+ }
154
+
155
+ function renderRow(row) {
156
+ const canDetail = boot.canDetail !== false;
157
+ const fire = row.list_skill === "fire" ? " is-fire" : "";
158
+ const tagKeys = String(row.keys || "").toLowerCase() === "5k" ? '<span class="song-tag">(5K)</span>' : "";
159
+ const mutation = (row.score_mutation || "").trim();
160
+ const tagMut = mutation && mutation !== "无" ? `<span class="song-tag">[${escapeHtml(mutation)}]</span>` : "";
161
+ const bpm = bpmLabel(row);
162
+ const bpmHtml = bpm ? `<span class="song-row__bpm">BPM ${escapeHtml(bpm)}</span>` : "";
163
+ const inner = `
164
+ <span class="song-row__title-rail" data-title-marquee title="${escapeHtml(row.title)}">
165
+ <span class="song-row__title" data-title-marquee-text>${escapeHtml(row.title)}</span>
166
+ </span>
167
+ <span class="song-row__meta">
168
+ ${tagKeys}${tagMut}
169
+ <span class="song-row__star">${escapeHtml(row.star)}星</span>
170
+ </span>
171
+ ${bpmHtml}`;
172
+ if (!canDetail) {
173
+ return `<div class="song-row${fire}" role="listitem">${inner}</div>`;
174
+ }
175
+ return `<button type="button" class="song-row${fire}" role="listitem" data-song-pick
176
+ data-title="${escapeHtml(row.title)}"
177
+ data-artist="${escapeHtml(row.artist)}"
178
+ data-family="${escapeHtml(row.family)}"
179
+ data-family-label="${escapeHtml(row.family_label || "")}"
180
+ data-star="${escapeHtml(row.star)}"
181
+ data-bpm="${escapeHtml(row.bpm == null ? "" : row.bpm)}"
182
+ data-supports-double="${row.supports_double ? "1" : "0"}"
183
+ data-icon="${escapeHtml(iconFor(row))}"
184
+ data-detail-base="${escapeHtml(detailBase(row))}"
185
+ >${inner}</button>`;
186
+ }
187
+
188
+ function syncHint(visibleCount) {
189
+ if (countEl) countEl.textContent = String(visibleCount);
190
+ const qDisplay = (searchInput && searchInput.value ? searchInput.value : filterState.q || "").trim();
191
+ if (qHint) {
192
+ qHint.textContent = qDisplay ? `[${qDisplay}]` : "";
193
+ qHint.hidden = !qDisplay;
194
+ }
195
+ if (hintTail) {
196
+ if (fuzzySuggest && qDisplay) {
197
+ hintTail.textContent = "。未精确匹配,是否你想找以下结果?";
198
+ } else if (qDisplay) {
199
+ hintTail.textContent = "。点击歌曲选择查看类别。";
200
+ } else if (filterState.star || filterState.letter || filterState.bpmBucket || filterState.bpmExact) {
201
+ hintTail.textContent = "。可继续输入关键词缩小范围。";
202
+ } else {
203
+ hintTail.textContent = "。点左侧字母或选择星级/BPM 浏览本模式全部歌曲。";
204
+ }
205
+ }
206
+ }
207
+
208
+ function syncLetterField(letter) {
209
+ const form = document.querySelector(".query-filters");
210
+ if (!form) return;
211
+ let input = form.querySelector("[data-letter-field]");
212
+ const value = (letter || "").trim();
213
+ if (!value) {
214
+ if (input) input.remove();
215
+ return;
216
+ }
217
+ if (!input) {
218
+ input = document.createElement("input");
219
+ input.type = "hidden";
220
+ input.name = "letter";
221
+ input.setAttribute("data-letter-field", "");
222
+ form.appendChild(input);
223
+ }
224
+ input.value = value;
225
+ }
226
+
227
+ function applyFilters() {
228
+ if (!window.CatalogSearch) return;
229
+ const q = (searchInput && searchInput.value ? searchInput.value : filterState.q || "").trim();
230
+ filterState.q = q;
231
+ filterState.star = (starNative && starNative.value) || filterState.star || "";
232
+ filterState.bpmBucket = (bpmBucketInput && bpmBucketInput.value) || filterState.bpmBucket || "";
233
+ filterState.bpmExact = (bpmExactField && bpmExactField.value) || filterState.bpmExact || "";
234
+ if (q) filterState.letter = "";
235
+ const ranked = window.CatalogSearch.searchCatalog(songs, q, filterState);
236
+ fuzzySuggest = Boolean(q && ranked.length && ranked.every((item) => item.fuzzy));
237
+ const html = ranked.map((item) => renderRow(item.entry)).join("");
238
+ const keepEmpty = emptyHint;
239
+ songList.querySelectorAll(".song-row").forEach((el) => el.remove());
240
+ if (keepEmpty) {
241
+ keepEmpty.hidden = ranked.length > 0;
242
+ keepEmpty.insertAdjacentHTML("afterend", html);
243
+ } else {
244
+ songList.insertAdjacentHTML("beforeend", html);
245
+ }
246
+ syncHint(ranked.length);
247
+ syncLetterField(filterState.letter);
248
+ document.querySelectorAll("[data-alpha-item]").forEach((el) => {
249
+ const active =
250
+ filterState.letter && el.textContent.trim().toLowerCase() === filterState.letter.toLowerCase();
251
+ el.classList.toggle("is-on", Boolean(active));
252
+ });
253
+ const params = new URLSearchParams(window.location.search);
254
+ if (family) params.set("family", family);
255
+ if (filterState.q) params.set("q", filterState.q);
256
+ else params.delete("q");
257
+ if (filterState.star) params.set("star", filterState.star);
258
+ else params.delete("star");
259
+ if (filterState.letter) params.set("letter", filterState.letter);
260
+ else params.delete("letter");
261
+ if (filterState.bpmExact) params.set("bpm_exact", filterState.bpmExact);
262
+ else params.delete("bpm_exact");
263
+ if (filterState.bpmBucket) params.set("bpm", filterState.bpmBucket);
264
+ else params.delete("bpm");
265
+ const qs = params.toString();
266
+ const target = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
267
+ window.history.replaceState(null, "", target);
268
+ songList.hidden = false;
269
+ songList.removeAttribute("data-filter-pending");
270
+ if (typeof window.bindTitleMarquees === "function") window.bindTitleMarquees();
271
+ }
272
+
273
+ window.__applyQueryFilters = function () {
274
+ applyFilters();
275
+ };
276
+
277
+ window.__setQueryLetterFilter = function (label) {
278
+ const next = (label || "").trim();
279
+ if (filterState.letter.toLowerCase() === next.toLowerCase()) filterState.letter = "";
280
+ else filterState.letter = next;
281
+ if (searchInput) {
282
+ searchInput.value = "";
283
+ filterState.q = "";
284
+ const clearBtn = searchInput.parentElement && searchInput.parentElement.querySelector("[data-search-clear]");
285
+ if (clearBtn) clearBtn.classList.remove("is-on");
286
+ }
287
+ applyFilters();
288
+ };
289
+
290
+ window.__queryLoadFamily = function (nextFamily, nextStar) {
291
+ const key = String(nextFamily || "").trim();
292
+ if (!key || !boot.familyUrls[key]) return false;
293
+ filterState.star = nextStar == null ? "" : String(nextStar || "");
294
+ if (starNative) {
295
+ starNative.value = filterState.star;
296
+ starNative.dispatchEvent(new Event("change"));
297
+ }
298
+ loadFamily(key).then(applyFilters);
299
+ return true;
300
+ };
301
+
302
+ if (searchInput) {
303
+ searchInput.addEventListener("input", () => {
304
+ filterState.q = searchInput.value.trim();
305
+ if (filterState.q) filterState.letter = "";
306
+ applyFilters();
307
+ });
308
+ searchInput.addEventListener("keydown", (e) => {
309
+ if (e.key === "Enter") {
310
+ e.preventDefault();
311
+ applyFilters();
312
+ }
313
+ });
314
+ }
315
+
316
+ loadFamily(family)
317
+ .then(applyFilters)
318
+ .catch(() => {
319
+ if (emptyHint) {
320
+ emptyHint.hidden = false;
321
+ emptyHint.textContent = "歌单加载失败,请刷新重试。";
322
+ }
323
+ songList.hidden = false;
324
+ songList.removeAttribute("data-filter-pending");
325
+ });
326
+
327
+ Object.keys(boot.familyUrls || {}).forEach((key) => {
328
+ if (key === family) return;
329
+ cachedFetch(boot.familyUrls[key]);
330
+ });
331
+ })();
@@ -0,0 +1 @@
1
+ {"v":1,"shared":{"sheet":"atlas/shared.webp","w":1544,"h":1542,"frames":{"pinball/UIAtlasPinball.png":[2,2,1024,1024,1024,1024],"pinball/font/ComboFontBlue.png":[1028,2,512,512,512,512],"pinball/font/ComboFontGreen.png":[2,1028,512,512,512,512],"pinball/font/ComboOrangeNum.png":[516,1028,512,512,512,512],"pinball/font/ComboFinalColorNum.png":[1028,516,512,256,512,256],"pinball/font/ComboLevelFive.png":[1030,1028,512,256,512,256],"hud/baoqi_bg_01.png":[1166,774,118,118,118,118],"hud/baoqi_bg_02.png":[1028,774,136,138,136,138]}},"modes":{"idol":{"sheet":"atlas/idol.webp","w":1028,"h":1107,"frames":{"idol/idol_ui_atlas.png":[2,2,1024,1024,1024,1024],"idol/idol_ribbon.png":[172,1028,40,40,80,80],"idol/idol_ribbon2.png":[2,1028,102,77,203,154],"idol/back_huadong.png":[106,1028,64,64,128,128],"idol/tiaodai.png":[214,1028,32,32,64,64]}},"crescent":{"sheet":"atlas/crescent.webp","w":3757,"h":516,"frames":{"crescent/UIAtlasInGameModeCrescentTrack.png":[1028,2,512,512,2048,2048],"crescent/track/crescent-track-1.png":[1542,2,441,478,882,956],"crescent/track/crescent-track-2.png":[1985,2,441,478,882,956],"crescent/track/crescent-track-3.png":[2428,2,441,478,882,956],"crescent/track/crescent-track-4.png":[2871,2,441,478,882,956],"crescent/track/crescent-track-5.png":[3314,2,441,478,882,956],"crescent/UIAtlasInGameModeCrescent.png":[2,2,1024,512,1024,512]}},"pinball":{"sheet":"atlas/pinball.webp","w":516,"h":2056,"frames":{"pinball/danzhu_putong.png":[110,1778,88,88,175,175],"pinball/danzhu_jiafen.png":[57,1964,64,64,128,128],"pinball/danzhu_pandingxian.png":[57,1778,51,94,101,188],"pinball/danzhu_qiangbi.png":[2,1544,53,510,106,1020],"pinball/danzhu_changentuowei.png":[57,1544,218,76,436,151],"pinball/danzhu_changentuowei_021.png":[327,1874,74,40,147,80],"pinball/danzhu_lianjiexian_01.png":[470,1778,23,15,45,29],"pinball/danzhu_lianjiexian_02.png":[497,1544,15,24,29,48],"pinball/danzhu_liuxing.png":[123,1964,64,64,128,128],"pinball/danzhu_fantan.png":[200,1778,88,88,175,175],"pinball/tex_ui_xulie_27.png":[2,2,512,512,512,512],"pinball/tex_ui_lizi_21.png":[189,1964,64,64,128,128],"pinball/tex_ui_lizi_32.png":[255,1964,64,64,128,128],"pinball/danzhu_yijie.png":[290,1778,88,88,175,175],"pinball/danzhu_changentuowei_yijie.png":[277,1544,218,76,436,151],"pinball/danzhu_erjie.png":[380,1778,88,88,175,175],"pinball/danzhu_changentuowei_erjie.png":[57,1622,218,76,436,151],"pinball/danzhu_sanjie.png":[57,1874,88,88,175,175],"pinball/danzhu_changentuowei_sanjie.png":[277,1622,218,76,436,151],"pinball/danzhu_sijie.png":[147,1874,88,88,175,175],"pinball/danzhu_changentuowei_sijie.png":[57,1700,218,76,436,151],"pinball/danzhu_wujie.png":[237,1874,88,88,175,175],"pinball/danzhu_changentuowei_wujie.png":[277,1700,218,76,436,151],"pinball/danzhu_jiafen.pink.png":[321,1964,64,64,128,128],"pinball/danzhu_jiafen.red.png":[387,1964,64,64,128,128],"pinball/danzhu_lianjiexian_02.pink.png":[497,1570,15,24,29,48],"pinball/danzhu_lianjiexian_02.red.png":[497,1596,15,24,29,48],"pinball/tex_ui_xulie_27.pink.png":[2,516,512,512,512,512],"pinball/tex_ui_xulie_27.red.png":[2,1030,512,512,512,512]}},"bubble":{"sheet":"atlas/bubble.webp","w":618,"h":702,"frames":{"bubble/pop_yellow.png":[72,2,107,107,214,214],"bubble/pop_yellow_bg.png":[181,2,107,107,214,214],"bubble/pop_green.png":[290,2,107,107,214,214],"bubble/pop_green_bg.png":[399,2,107,107,214,214],"bubble/pop_green_bigstar.png":[571,111,30,30,60,60],"bubble/pop_green_star.png":[206,534,61,25,122,50],"bubble/pop_blue.png":[508,2,107,107,214,214],"bubble/pop_blue_bg.png":[72,111,107,107,214,214],"bubble/pop_blue01.png":[72,534,66,66,132,132],"bubble/pop_lanse_chuansong.png":[2,2,68,600,136,1200],"bubble/pop_line.png":[72,514,79,18,158,36],"bubble/tex_ui_paopao_blue.png":[140,534,64,64,128,128],"bubble/pop_green_0.png":[181,111,76,96,76,96],"bubble/pop_green_1.png":[259,111,76,96,76,96],"bubble/pop_green_2.png":[337,111,76,96,76,96],"bubble/pop_green_3.png":[415,111,76,96,76,96],"bubble/pop_green_4.png":[493,111,76,96,76,96],"bubble/pop_green_5.png":[72,220,76,96,76,96],"bubble/pop_green_6.png":[150,220,76,96,76,96],"bubble/pop_green_7.png":[228,220,76,96,76,96],"bubble/pop_green_8.png":[306,220,76,96,76,96],"bubble/pop_green_9.png":[384,220,76,96,76,96],"bubble/pop_blue_0.png":[462,220,76,96,76,96],"bubble/pop_blue_1.png":[540,220,76,96,76,96],"bubble/pop_blue_2.png":[72,318,76,96,76,96],"bubble/pop_blue_3.png":[150,318,76,96,76,96],"bubble/pop_blue_4.png":[228,318,76,96,76,96],"bubble/pop_blue_5.png":[306,318,76,96,76,96],"bubble/pop_blue_6.png":[384,318,76,96,76,96],"bubble/pop_blue_7.png":[462,318,76,96,76,96],"bubble/pop_blue_8.png":[540,318,76,96,76,96],"bubble/pop_blue_9.png":[72,416,76,96,76,96],"bubble/pop_yellow_0.png":[150,416,76,96,76,96],"bubble/pop_yellow_1.png":[228,416,76,96,76,96],"bubble/pop_yellow_2.png":[306,416,76,96,76,96],"bubble/pop_yellow_3.png":[384,416,76,96,76,96],"bubble/pop_yellow_4.png":[462,416,76,96,76,96],"bubble/pop_yellow_5.png":[540,416,76,96,76,96],"bubble/pop_yellow_6.png":[2,604,76,96,76,96],"bubble/pop_yellow_7.png":[80,604,76,96,76,96],"bubble/pop_yellow_8.png":[158,604,76,96,76,96],"bubble/pop_yellow_9.png":[236,604,76,96,76,96]}}}}