V1.3.9: 二维码跟随控制层 + 声音设置精简 + 远程滑杆绑定修复 + 搜索提速

1) 播放界面右上角二维码改为跟随控制层出现/隐藏
   setFullScreenChromeVisible() 里同步 fullScreenQrOverlay 可见性,
   updateFullScreenQrOverlay() 新建时按 fullScreenChromeVisible 初始化;
   删除按"手机是否在点歌"自动隐藏的 qrAutoHideMonitor,
   并删除扫码点歌弹窗里的"扫码点歌后自动隐藏播放界面二维码"提示词。

2) 声音设置删除音量调节与均衡器入口
   移除「歌曲最大音量」滑杆(及 pendingMusicVolume)与「均衡器入口」按钮,
   该弹窗只保留「切到点播歌曲时音量/原伴唱」这类策略选择; 音量与均衡器统一在「调音」弹窗。

3) 远程控制均衡器滑动点未完全绑定的真正原因: 滑杆被 <label> 包裹
   <label> 会把内部任意位置的点击转发给 range -> 点轨道/点文字就跳值。
   行容器 label -> div(.audio-row / .eq-band), CSS 相应改为类选择器;
   滑杆元素高度 4px -> 32px(触控目标), 细轨道改由 ::-webkit-slider-runnable-track 绘制,
   滑块 22px+5px 边框; attachThumbOnlyDrag 命中容差按 32px 滑块对齐(第二道防线)。

4) 远程搜歌界面: 歌曲移到左边、歌手移到右边
   同步把默认搜索方式改为「歌曲」(否则左侧高亮与实际模式不一致), 初始 placeholder 同步。

5) 搜索提速(基于对 991MB/67 万首 muse.db 的实测)
   实测: 中文 contains 热态 2~280ms; 拼音 GLOB 热态 0.2~150ms、冷态 1~15s;
   歌手 EXISTS 34~70ms。瓶颈是冷态 I/O 与重复查库, 因此:
   - 设备端新增 remoteSearchCache(LRU 24 条=4 查询×6 页, TTL 60s, 曲库刷新即清空),
     同一「类型+关键词+页码」直接复用 JSON, 翻页/重复搜索不再查库;
   - 手机端: AbortController 取消旧请求、同关键词不重复请求、首页返回后预热第 2 页、
     搜索期间暂停 1s 状态轮询避免抢 I/O;
   - 实测证明「一次多取几页」更慢(301 行 6.2s vs 21 行 0.7s), 故保持单页 LIMIT;
   - 均衡器卡片收起时跳过其 DOM 刷新(展开时补渲染)。

版本号 1.3.8 -> 1.3.9(versionCode 14 -> 15)。按约定本次不发布 Release, 仅本地打包测试。
This commit is contained in:
zijunchiang
2026-09-20 19:36:41 +08:00
parent 42919cc8e3
commit 33b704bd5a
6 changed files with 113 additions and 88 deletions
+42 -15
View File
@@ -1,9 +1,10 @@
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => document.querySelectorAll(selector);
const uiState = {
page: 1, query: "", searchMode: "singer", busy: false,
page: 1, query: "", searchMode: "song", busy: false,
hotPage: 1, hotPageSize: 10, hotBusy: false,
visible: true, connected: false, eqEnabled: false
visible: true, connected: false, eqEnabled: false,
equalizer: null, searchAbort: null, searchKey: ""
};
function escapeHtml(value) {
@@ -81,7 +82,9 @@ async function refresh() {
$("#original").classList.toggle("active", data.vocalMode === "original");
$("#accompany").classList.toggle("active", data.vocalMode !== "original");
$("#nextSong").textContent = data.next?.title || "队列为空";
renderEqualizer(data.equalizer);
// 均衡器卡片默认收起: 收起时跳过 DOM 更新(避免每秒轮询重绘), 展开时再渲染一次。
uiState.equalizer = data.equalizer;
if (!$("#eqBody")?.classList.contains("hidden")) renderEqualizer(data.equalizer);
} catch (_) {
setConnected(false);
}
@@ -107,7 +110,7 @@ function renderEqualizer(eq) {
const bands = Array.isArray(eq.bands) ? eq.bands : [];
const container = $("#eqBands");
if (container && container.childElementCount !== bands.length) {
container.innerHTML = bands.map((band, index) => `<label class="eq-band"><span>${escapeHtml(band.label)}Hz</span><b><em id="eqValue${index}">${Number(band.level).toFixed(1)}</em>dB</b><input id="eqBand${index}" type="range" min="${eq.minDb}" max="${eq.maxDb}" step="1" value="${band.level}"></label>`).join("");
container.innerHTML = bands.map((band, index) => `<div class="eq-band"><span>${escapeHtml(band.label)}Hz</span><b><em id="eqValue${index}">${Number(band.level).toFixed(1)}</em>dB</b><input id="eqBand${index}" type="range" min="${eq.minDb}" max="${eq.maxDb}" step="1" value="${band.level}"></div>`).join("");
bands.forEach((band, index) => {
const input = $("#eqBand" + index);
if (!input) return;
@@ -160,7 +163,7 @@ function attachThumbOnlyDrag(input) {
const x = point.clientX;
const y = point.clientY;
// 粗略还原轨道几何:控件左右各留半个滑块宽度。
const thumb = 22;
const thumb = 32;
const trackLeft = rect.left + thumb / 2;
const trackRight = rect.right - thumb / 2;
const span = trackRight - trackLeft;
@@ -200,31 +203,52 @@ async function updateAudio(values) {
}
async function search(reset = true) {
if (uiState.busy) return;
if (reset) {
uiState.page = 1;
uiState.query = $("#query").value.trim();
if (!uiState.query) {
const nextQuery = $("#query").value.trim();
if (!nextQuery) {
toast(`请输入${uiState.searchMode === "singer" ? "歌手" : "歌曲"}名称`);
$("#query").focus();
return;
}
// 同一关键词+同一搜索方式且结果已渲染 → 不重复请求设备(设备端也要查 1GB 曲库)。
const key = `${uiState.searchMode}|${nextQuery}`;
if (key === uiState.searchKey && $("#results").childElementCount) {
$("#searchResultBlock").scrollIntoView({ behavior: "smooth", block: "start" });
return;
}
uiState.page = 1;
uiState.query = nextQuery;
uiState.searchKey = key;
$("#results").innerHTML = "";
$("#searchResultBlock").classList.remove("hidden");
$("#resultTitle").textContent = uiState.searchMode === "singer" ? "歌手搜索结果" : "歌曲搜索结果";
$("#resultSummary").textContent = `正在搜索“${uiState.query}”`;
$("#more").classList.add("hidden");
if (uiState.searchAbort) uiState.searchAbort.abort();
uiState.searchAbort = new AbortController();
}
const signal = uiState.searchAbort ? uiState.searchAbort.signal : undefined;
const requestedPage = uiState.page;
uiState.busy = true;
try {
const data = await api(`/api/v1/songs?q=${encodeURIComponent(uiState.query)}&type=${uiState.searchMode}&page=${uiState.page}&pageSize=20`);
const rows = (data.songs || []).map((song, index) => songRow(song, false, false, (uiState.page - 1) * 20 + index)).join("");
const data = await api(`/api/v1/songs?q=${encodeURIComponent(uiState.query)}&type=${uiState.searchMode}&page=${requestedPage}&pageSize=20`, { signal });
const rows = (data.songs || []).map((song, index) => songRow(song, false, false, (requestedPage - 1) * 20 + index)).join("");
if (rows) $("#results").insertAdjacentHTML("beforeend", rows);
else if (reset) $("#results").innerHTML = '<div class="empty">没有找到相关歌曲<br><small>请更换关键词后重试</small></div>';
$("#resultSummary").textContent = `“${uiState.query}”${data.hasMore ? ",还有更多" : ""}`;
$("#more").classList.toggle("hidden", !data.hasMore);
uiState.page += 1;
} catch (error) { toast(error.message); }
finally { uiState.busy = false; }
uiState.page = requestedPage + 1;
// 预热下一页: 设备端会把它缓存下来, 用户点「更多」时基本瞬时。
if (requestedPage === 1 && data.hasMore) {
window.setTimeout(() => {
api(`/api/v1/songs?q=${encodeURIComponent(uiState.query)}&type=${uiState.searchMode}&page=2&pageSize=20`)
.catch(() => {});
}, 500);
}
} catch (error) {
// 被下一次搜索取消属于正常情况, 不弹提示。
if (error && error.name !== "AbortError") toast(error.message);
} finally { uiState.busy = false; }
}
async function loadHot(page) {
@@ -317,8 +341,11 @@ $("#eqCollapse")?.addEventListener("click", () => {
if (!body) return;
const collapsed = body.classList.toggle("hidden");
$("#eqCollapse").setAttribute("aria-expanded", collapsed ? "false" : "true");
// 展开时补一次渲染(收起期间轮询跳过了 EQ 的 DOM 更新)。
if (!collapsed) renderEqualizer(uiState.equalizer);
});
document.addEventListener("visibilitychange", () => { uiState.visible = !document.hidden; if (uiState.visible) refresh(); });
window.setInterval(() => { if (uiState.visible) refresh(); }, 1000);
// 搜索进行中暂停状态轮询: 避免与设备端 1GB 曲库查询抢 I/O。
window.setInterval(() => { if (uiState.visible && !uiState.busy) refresh(); }, 1000);
refresh();
loadHot(1);