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.
- package/README.md +4 -4
- package/js/auth-shell-sync.js +45 -45
- package/js/burst-preview-host.js +350 -212
- package/js/catalog-search.js +311 -311
- package/js/query-app.js +331 -331
- package/note-preview/assets/atlas/manifest.json +1 -1
- package/note-preview/note-preview.min.js +1 -1
- package/package.json +18 -18
- package/catalog/bubble-4f53cda18c2b.json +0 -1
- package/catalog/crescent-4f53cda18c2b.json +0 -1
- package/catalog/dianfeng-4f53cda18c2b.json +0 -1
- package/catalog/manifest.json +0 -1
- package/catalog/pinball-4f53cda18c2b.json +0 -1
- package/catalog/pinyin-4f53cda18c2b.json +0 -1
- package/catalog/xingdong-4f53cda18c2b.json +0 -1
package/js/query-app.js
CHANGED
|
@@ -1,331 +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, "&")
|
|
96
|
-
.replace(/</g, "<")
|
|
97
|
-
.replace(/>/g, ">")
|
|
98
|
-
.replace(/"/g, """);
|
|
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
|
-
})();
|
|
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, "&")
|
|
96
|
+
.replace(/</g, "<")
|
|
97
|
+
.replace(/>/g, ">")
|
|
98
|
+
.replace(/"/g, """);
|
|
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
|
+
})();
|