759 lines
34 KiB
JavaScript
759 lines
34 KiB
JavaScript
/* ============================================================
|
|
Xiao の 电台 — Enhanced JS
|
|
============================================================ */
|
|
|
|
// ==================== Toast ====================
|
|
function showToast(msg, type = "") {
|
|
const c = document.getElementById("toastContainer");
|
|
const el = document.createElement("div");
|
|
el.className = "toast-msg" + (type ? " " + type : "");
|
|
el.textContent = msg;
|
|
c.appendChild(el);
|
|
setTimeout(() => el.remove(), 3000);
|
|
}
|
|
|
|
// ==================== Navigation ====================
|
|
const tabs = document.querySelectorAll(".nav-tab");
|
|
const pages = document.querySelectorAll(".page");
|
|
|
|
function switchPage(pageId) {
|
|
pages.forEach((p) => p.classList.remove("active"));
|
|
tabs.forEach((t) => t.classList.remove("active"));
|
|
const page = document.getElementById(pageId);
|
|
if (page) page.classList.add("active");
|
|
tabs.forEach((t) => {
|
|
if (t.dataset.page === pageId) t.classList.add("active");
|
|
});
|
|
// Eager-load data on tab switch
|
|
if (pageId === "pageResources") loadResources();
|
|
if (pageId === "pagePlaylist") loadPlaylist();
|
|
}
|
|
tabs.forEach((tab) =>
|
|
tab.addEventListener("click", () =>
|
|
switchPage(tab.dataset.page),
|
|
),
|
|
);
|
|
|
|
// ==================== Helpers ====================
|
|
function esc(s) {
|
|
return String(s ?? "").replace(
|
|
/[&<>"']/g,
|
|
(c) =>
|
|
({
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
})[c],
|
|
);
|
|
}
|
|
function formatDuration(sec) {
|
|
if (!sec && sec !== 0) return "--:--";
|
|
const s = Math.round(sec),
|
|
m = Math.floor(s / 60),
|
|
ss = s % 60;
|
|
return m + ":" + String(ss).padStart(2, "0");
|
|
}
|
|
function formatBytes(b) {
|
|
if (!b) return "0 B";
|
|
if (b >= 1048576) return (b / 1048576).toFixed(2) + " MB";
|
|
if (b >= 1024) return (b / 1024).toFixed(1) + " KB";
|
|
return b + " B";
|
|
}
|
|
async function api(url, options = {}) {
|
|
const res = await fetch(url, options);
|
|
if (!res.ok) throw new Error(await res.text());
|
|
return res.json();
|
|
}
|
|
|
|
// ==================== ID Extraction ====================
|
|
function extractMusicId(input) {
|
|
input = input.trim();
|
|
if (!input) return "";
|
|
if (/^\d+$/.test(input)) return input;
|
|
const patterns = [
|
|
/music\.163\.com\/#\/song\?.*id=(\d+)/,
|
|
/music\.163\.com\/song\?.*id=(\d+)/,
|
|
/music\.163\.com\/song\/(\d+)/,
|
|
/y\.music\.163\.com\/m\/song\?.*id=(\d+)/,
|
|
/[?&]id=(\d+)/,
|
|
/(\d{5,})/,
|
|
];
|
|
for (const p of patterns) {
|
|
const m = input.match(p);
|
|
if (m) return m[1];
|
|
}
|
|
return input;
|
|
}
|
|
|
|
// ==================== Parse Hint ====================
|
|
const musicIdInput = document.getElementById("musicId");
|
|
const parseHint = document.getElementById("parseHint");
|
|
const parsedIdEl = document.getElementById("parsedId");
|
|
function updateParseHint() {
|
|
const val = musicIdInput.value.trim(),
|
|
extracted = extractMusicId(val);
|
|
if (val && extracted !== val && /^\d+$/.test(extracted)) {
|
|
parsedIdEl.textContent = extracted;
|
|
parseHint.classList.add("visible");
|
|
} else {
|
|
parseHint.classList.remove("visible");
|
|
}
|
|
}
|
|
musicIdInput.addEventListener("input", updateParseHint);
|
|
musicIdInput.addEventListener("paste", () =>
|
|
requestAnimationFrame(updateParseHint),
|
|
);
|
|
|
|
// ==================== List Diff ====================
|
|
function reconcileList(container, items, keyOf, create, update) {
|
|
const nodes = Array.from(
|
|
container.querySelectorAll("[data-key]"),
|
|
);
|
|
const byKey = new Map(nodes.map((n) => [n.dataset.key, n]));
|
|
const seen = new Set();
|
|
items.forEach((item, idx) => {
|
|
const key = String(keyOf(item));
|
|
seen.add(key);
|
|
let el = byKey.get(key);
|
|
if (!el) {
|
|
el = create(item);
|
|
el.dataset.key = key;
|
|
el.classList.add("enter");
|
|
}
|
|
update(el, item, idx);
|
|
if (container.children[idx] !== el)
|
|
container.insertBefore(
|
|
el,
|
|
container.children[idx] || null,
|
|
);
|
|
});
|
|
nodes.forEach((el) => {
|
|
if (!seen.has(el.dataset.key)) {
|
|
el.classList.add("leave");
|
|
el.addEventListener("animationend", () => el.remove(), {
|
|
once: true,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// ==================== Resources ====================
|
|
function renderResItem(item) {
|
|
const cover = item.albumImage
|
|
? `<img class="res-cover" src="${esc(item.albumImage)}" alt="" loading="lazy">`
|
|
: `<div class="res-cover-ph"><i class="bi bi-music-note"></i></div>`;
|
|
const bg = item.albumImage
|
|
? `<div class="res-bg" style="background-image:url('${esc(item.albumImage)}')"></div>`
|
|
: "";
|
|
const tpl = document.createElement("template");
|
|
tpl.innerHTML = `<div class="res-item">${bg}
|
|
<div class="res-row">${cover}
|
|
<div class="res-meta">
|
|
<div class="res-title" title="${esc(item.title)}">${esc(item.title)}</div>
|
|
<div class="res-artist">${esc(item.artist) || "未知作者"}</div>
|
|
</div>
|
|
<div class="res-actions">
|
|
<span class="heat-chip"><i class="bi bi-fire"></i><span class="js-heat">${item.heat}</span></span>
|
|
<button class="btn-icon" onclick="addToPlaylist('${esc(item.file)}')" title="添加到播放列表">
|
|
<i class="bi bi-plus-circle-fill" style="color:var(--accent)"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="res-detail">
|
|
<span><i class="bi bi-clock"></i> ${formatDuration(item.duration)}</span>
|
|
<span class="detail-sep">|</span>
|
|
<span><i class="bi bi-file-earmark"></i> ${formatBytes(item.fileSize)}</span>
|
|
<span class="detail-sep">|</span>
|
|
<span><i class="bi bi-database"></i> 缓存 <span class="js-cache">${formatBytes(item.cacheSize)}</span></span>
|
|
</div>
|
|
</div>`;
|
|
return tpl.content.firstElementChild;
|
|
}
|
|
function updateResItem(el, item) {
|
|
const h = el.querySelector(".js-heat");
|
|
if (h.textContent !== String(item.heat))
|
|
h.textContent = item.heat;
|
|
const c = el.querySelector(".js-cache"),
|
|
cs = formatBytes(item.cacheSize);
|
|
if (c.textContent !== cs) c.textContent = cs;
|
|
}
|
|
let resourceKeyword = "";
|
|
function applyResourceFilter() {
|
|
document
|
|
.getElementById("resourceList")
|
|
.querySelectorAll(".res-item")
|
|
.forEach((el) => {
|
|
const t = (
|
|
el.querySelector(".res-title")?.textContent || ""
|
|
).toLowerCase();
|
|
const a = (
|
|
el.querySelector(".res-artist")?.textContent || ""
|
|
).toLowerCase();
|
|
el.style.display =
|
|
!resourceKeyword ||
|
|
t.includes(resourceKeyword) ||
|
|
a.includes(resourceKeyword)
|
|
? ""
|
|
: "none";
|
|
});
|
|
}
|
|
async function loadResources() {
|
|
try {
|
|
const resources = await api("/api/resources");
|
|
const container = document.getElementById("resourceList");
|
|
document.getElementById("resourceCount").textContent =
|
|
resources.length;
|
|
if (resources.length === 0) {
|
|
container.innerHTML = `<div class="empty-state"><div class="empty-icon"><i class="bi bi-folder-x"></i></div><div class="empty-title">暂无资源</div><div class="empty-desc">前往搜索页导入音乐</div></div>`;
|
|
return;
|
|
}
|
|
if (!container.querySelector("[data-key]"))
|
|
container.innerHTML = "";
|
|
reconcileList(
|
|
container,
|
|
resources,
|
|
(r) => r.file,
|
|
renderResItem,
|
|
updateResItem,
|
|
);
|
|
applyResourceFilter();
|
|
} catch (err) {
|
|
console.error("加载资源失败:", err);
|
|
}
|
|
}
|
|
|
|
// ==================== Playlist ====================
|
|
let sortableInstance = null;
|
|
function renderPlItem(item) {
|
|
const tpl = document.createElement("template");
|
|
tpl.innerHTML = `<div class="pl-item" data-id="${esc(item.path)}">
|
|
<i class="bi bi-grip-vertical pl-grip"></i>
|
|
<span class="pl-num"></span>
|
|
<div class="pl-info">
|
|
<div class="pl-name" title="${esc(item.name)}">${esc(item.name)}</div>
|
|
<div class="pl-sub">
|
|
<span class="heat-chip pl-heat"><i class="bi bi-fire"></i><span class="js-heat"></span></span>
|
|
<span class="pl-cache"></span>
|
|
</div>
|
|
</div>
|
|
<button class="btn-icon danger" title="移除"><i class="bi bi-x-circle-fill"></i></button>
|
|
</div>`;
|
|
return tpl.content.firstElementChild;
|
|
}
|
|
function updatePlItem(el, item, idx) {
|
|
const numEl = el.querySelector(".pl-num");
|
|
if (numEl.textContent !== String(idx + 1))
|
|
numEl.textContent = idx + 1;
|
|
const heatEl = el.querySelector(".js-heat");
|
|
if (heatEl.textContent !== String(item.heat))
|
|
heatEl.textContent = item.heat;
|
|
const cacheEl = el.querySelector(".pl-cache");
|
|
const st =
|
|
item.cacheStatus === "ready"
|
|
? "ready"
|
|
: item.cacheStatus === "pending"
|
|
? "pending"
|
|
: "none";
|
|
if (cacheEl.dataset.status !== st) {
|
|
cacheEl.dataset.status = st;
|
|
cacheEl.className = "pl-cache " + st;
|
|
cacheEl.innerHTML =
|
|
st === "ready"
|
|
? '<i class="bi bi-check-circle-fill"></i> 已缓存'
|
|
: st === "pending"
|
|
? '<i class="bi bi-hourglass-split"></i> 生成中'
|
|
: '<i class="bi bi-cloud"></i> 未缓存';
|
|
}
|
|
el.querySelector("button").setAttribute(
|
|
"onclick",
|
|
"removeFromPlaylist(" + idx + ")",
|
|
);
|
|
}
|
|
async function loadPlaylist() {
|
|
try {
|
|
const playlistData = await api("/api/playlist");
|
|
const container = document.getElementById("playlistList");
|
|
document.getElementById("playlistCount").textContent =
|
|
playlistData.length;
|
|
if (playlistData.length === 0) {
|
|
container.innerHTML = `<div class="empty-state"><div class="empty-icon"><i class="bi bi-music-note"></i></div><div class="empty-title">播放列表为空</div><div class="empty-desc">从资源列表中添加音乐</div></div>`;
|
|
if (sortableInstance) {
|
|
sortableInstance.destroy();
|
|
sortableInstance = null;
|
|
}
|
|
return;
|
|
}
|
|
if (!container.querySelector("[data-key]"))
|
|
container.innerHTML = "";
|
|
reconcileList(
|
|
container,
|
|
playlistData,
|
|
(p) => p.path,
|
|
renderPlItem,
|
|
updatePlItem,
|
|
);
|
|
if (!sortableInstance) {
|
|
sortableInstance = new Sortable(container, {
|
|
animation: 200,
|
|
ghostClass: "sortable-ghost",
|
|
handle: ".pl-grip",
|
|
delay: 100,
|
|
delayOnTouchOnly: true,
|
|
onEnd: async () => {
|
|
const items = [
|
|
...container.querySelectorAll(".pl-item"),
|
|
];
|
|
const newOrder = items.map(
|
|
(el) => el.dataset.id,
|
|
);
|
|
try {
|
|
await api("/api/playlist/reorder", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({ newOrder }),
|
|
});
|
|
items.forEach((el, i) => {
|
|
el.querySelector(
|
|
".pl-num",
|
|
).textContent = i + 1;
|
|
el.querySelector("button").setAttribute(
|
|
"onclick",
|
|
"removeFromPlaylist(" + i + ")",
|
|
);
|
|
});
|
|
} catch (err) {
|
|
console.error("排序更新失败:", err);
|
|
loadPlaylist();
|
|
}
|
|
},
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error("加载播放列表失败:", err);
|
|
}
|
|
}
|
|
|
|
// ==================== Actions ====================
|
|
async function addToPlaylist(fileName) {
|
|
try {
|
|
await api("/api/playlist/add", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ file: fileName }),
|
|
});
|
|
showToast("已添加到播放列表", "success");
|
|
await loadPlaylist();
|
|
await loadResources();
|
|
} catch (err) {
|
|
showToast("添加失败: " + err.message, "error");
|
|
}
|
|
}
|
|
async function removeFromPlaylist(index) {
|
|
try {
|
|
await api("/api/playlist/remove", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ index }),
|
|
});
|
|
await loadPlaylist();
|
|
} catch (err) {
|
|
showToast("移除失败: " + err.message, "error");
|
|
}
|
|
}
|
|
async function clearPlaylist() {
|
|
if (!confirm("确定清空播放列表?清空后将自动随机添加一首。"))
|
|
return;
|
|
try {
|
|
await api("/api/playlist/clear", { method: "POST" });
|
|
await loadPlaylist();
|
|
} catch (err) {
|
|
showToast("清空失败: " + err.message, "error");
|
|
}
|
|
}
|
|
|
|
// ==================== Cache Info ====================
|
|
async function loadCacheInfo() {
|
|
try {
|
|
const info = await api("/api/cache/size");
|
|
document.getElementById("cacheInfo").innerHTML =
|
|
`<i class="bi bi-database"></i> ${(info.total / 1048576).toFixed(1)}/${(info.limit / 1048576).toFixed(0)}MB (${info.usedPercent}%)`;
|
|
} catch (err) {
|
|
console.error("加载缓存信息失败:", err);
|
|
}
|
|
}
|
|
|
|
// ==================== Mini Player ==================== //
|
|
const musicPlayer = document.getElementById("musicPlayer");
|
|
const miniPlayer = document.getElementById("miniPlayer");
|
|
const mpBody = document.getElementById("mpBody");
|
|
const mpProgress = document.getElementById("mpProgress");
|
|
const mpCover = document.getElementById("mpCover");
|
|
const mpName = document.getElementById("mpName");
|
|
const mpArtist = document.getElementById("mpArtist");
|
|
const mpPlay = document.getElementById("mpPlay");
|
|
const mpTime = document.getElementById("mpTime");
|
|
const mpExpand = document.getElementById("mpExpand");
|
|
|
|
function showMiniPlayer(info) {
|
|
mpName.textContent = info.name || "未知";
|
|
mpArtist.textContent = info.artist || "--";
|
|
if (info.albumImage) {
|
|
mpCover.src = info.albumImage;
|
|
mpCover.style.display = "";
|
|
} else {
|
|
mpCover.style.display = "none";
|
|
}
|
|
miniPlayer.classList.add("show");
|
|
document.body.classList.add("mini-player-visible");
|
|
}
|
|
function hideMiniPlayer() {
|
|
miniPlayer.classList.remove("show");
|
|
document.body.classList.remove("mini-player-visible");
|
|
}
|
|
|
|
mpPlay.addEventListener("click", () => {
|
|
if (musicPlayer.paused) musicPlayer.play();
|
|
else musicPlayer.pause();
|
|
});
|
|
musicPlayer.addEventListener("play", () => {
|
|
mpPlay.innerHTML = '<i class="bi bi-pause-fill"></i>';
|
|
});
|
|
musicPlayer.addEventListener("pause", () => {
|
|
mpPlay.innerHTML = '<i class="bi bi-play-fill"></i>';
|
|
});
|
|
musicPlayer.addEventListener("timeupdate", () => {
|
|
if (!musicPlayer.duration) return;
|
|
const pct =
|
|
(musicPlayer.currentTime / musicPlayer.duration) * 100;
|
|
mpProgress.style.width = pct + "%";
|
|
mpTime.textContent = formatDuration(musicPlayer.currentTime);
|
|
});
|
|
musicPlayer.addEventListener("ended", () => {
|
|
mpPlay.innerHTML = '<i class="bi bi-play-fill"></i>';
|
|
});
|
|
|
|
mpExpand.addEventListener("click", () => {
|
|
switchPage("pageSearch");
|
|
const preview = document.getElementById("musicPreview");
|
|
if (preview.style.display !== "none")
|
|
preview.scrollIntoView({
|
|
behavior: "smooth",
|
|
block: "center",
|
|
});
|
|
});
|
|
|
|
// ---- Drag-to-seek on the entire mini player bar ----
|
|
let isSeeking = false;
|
|
function seekFromPointer(e) {
|
|
const rect = mpBody.getBoundingClientRect();
|
|
const x = e.touches ? e.touches[0].clientX : e.clientX;
|
|
const ratio = Math.max(
|
|
0,
|
|
Math.min(1, (x - rect.left) / rect.width),
|
|
);
|
|
if (musicPlayer.duration) {
|
|
musicPlayer.currentTime = ratio * musicPlayer.duration;
|
|
mpProgress.style.width = ratio * 100 + "%";
|
|
}
|
|
}
|
|
mpBody.addEventListener("mousedown", (e) => {
|
|
if (e.target.closest("button")) return;
|
|
isSeeking = true;
|
|
seekFromPointer(e);
|
|
});
|
|
mpBody.addEventListener(
|
|
"touchstart",
|
|
(e) => {
|
|
if (e.target.closest("button")) return;
|
|
isSeeking = true;
|
|
seekFromPointer(e);
|
|
},
|
|
{ passive: true },
|
|
);
|
|
document.addEventListener("mousemove", (e) => {
|
|
if (isSeeking) seekFromPointer(e);
|
|
});
|
|
document.addEventListener(
|
|
"touchmove",
|
|
(e) => {
|
|
if (isSeeking) seekFromPointer(e);
|
|
},
|
|
{ passive: true },
|
|
);
|
|
document.addEventListener("mouseup", () => {
|
|
isSeeking = false;
|
|
});
|
|
document.addEventListener("touchend", () => {
|
|
isSeeking = false;
|
|
});
|
|
|
|
// ==================== Music Search ====================
|
|
let musicInfo = null,
|
|
selectedId = null;
|
|
|
|
async function showDetail(id) {
|
|
const info = await api("/api/music/detail", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ id }),
|
|
});
|
|
musicInfo = info;
|
|
selectedId = id;
|
|
document.getElementById("musicName").textContent = info.name;
|
|
document.getElementById("musicArtist").textContent =
|
|
info.artist || "未知作者";
|
|
document.getElementById("musicCover").src =
|
|
info.albumImage || "";
|
|
|
|
let tags = "";
|
|
if (info.album)
|
|
tags += `<span class="detail-tag album-tag"><i class="bi bi-disc"></i> ${esc(info.album)}</span>`;
|
|
if (info.duration)
|
|
tags += `<span class="detail-tag duration-tag"><i class="bi bi-clock"></i> ${formatDuration(info.duration)}</span>`;
|
|
if (info.quality)
|
|
tags += `<span class="detail-tag quality-tag"><i class="bi bi-badge-hd"></i> ${esc(info.quality)}</span>`;
|
|
if (info.fileSize)
|
|
tags += `<span class="detail-tag"><i class="bi bi-file-earmark-binary"></i> ${formatBytes(info.fileSize)}</span>`;
|
|
if (info.publishTime) {
|
|
const d = new Date(info.publishTime);
|
|
tags += `<span class="detail-tag"><i class="bi bi-calendar3"></i> ${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, "0")}.${String(d.getDate()).padStart(2, "0")}</span>`;
|
|
}
|
|
if (id)
|
|
tags += `<span class="detail-tag"><i class="bi bi-hash"></i> ${id}</span>`;
|
|
document.getElementById("musicDetails").innerHTML = tags;
|
|
|
|
document.getElementById("musicPreview").style.display = "block";
|
|
document.getElementById("searchTips").style.display = "none";
|
|
|
|
musicPlayer.pause();
|
|
musicPlayer.src =
|
|
"http://music.163.com/song/media/outer/url?id=" +
|
|
id +
|
|
".mp3";
|
|
|
|
showMiniPlayer({
|
|
name: info.name,
|
|
artist: info.artist,
|
|
albumImage: info.albumImage,
|
|
});
|
|
mpProgress.style.width = "0%";
|
|
mpTime.textContent = "0:00";
|
|
}
|
|
|
|
function renderSearchResults(data, keyword) {
|
|
const container = document.getElementById("searchResults");
|
|
musicInfo = null;
|
|
selectedId = null;
|
|
document.getElementById("musicPreview").style.display = "none";
|
|
|
|
if (!data.list || data.list.length === 0) {
|
|
container.innerHTML = `<div class="empty-state"><div class="empty-icon"><i class="bi bi-search"></i></div><div class="empty-title">未找到相关歌曲</div><div class="empty-desc">换个关键词试试</div></div>`;
|
|
container.style.display = "block";
|
|
document.getElementById("searchTips").style.display =
|
|
"none";
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<div class="sr-head">"${esc(keyword)}" 共 <b>${data.total}</b> 首相关歌曲,点击选择:</div>
|
|
<div class="sr-list">${data.list
|
|
.map(
|
|
(item, i) => `
|
|
<div class="sr-item" data-id="${item.id}" style="animation-delay:${i * 0.04}s">
|
|
<div class="sr-main">
|
|
<div class="sr-name">${esc(item.name)}</div>
|
|
<div class="sr-artist">${esc(item.artists.join(" / ") || "未知作者")}</div>
|
|
</div>
|
|
${item.alias && item.alias[0] ? `<div class="sr-alias" title="${esc(item.alias.join(" / "))}">${esc(item.alias[0])}</div>` : ""}
|
|
<div class="sr-duration">${formatDuration(item.duration / 1000)}</div>
|
|
</div>`,
|
|
)
|
|
.join("")}</div>`;
|
|
container.style.display = "block";
|
|
document.getElementById("searchTips").style.display = "none";
|
|
|
|
container.querySelectorAll(".sr-item").forEach((el) => {
|
|
el.addEventListener("click", () => {
|
|
container
|
|
.querySelectorAll(".sr-item")
|
|
.forEach((x) => x.classList.remove("active"));
|
|
el.classList.add("active");
|
|
showDetail(el.dataset.id).catch((err) =>
|
|
showToast("查询失败: " + err.message, "error"),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function searchMusic() {
|
|
const rawInput = musicIdInput.value.trim();
|
|
if (!rawInput)
|
|
return showToast("请输入歌曲 ID、链接或歌名", "error");
|
|
const id = extractMusicId(rawInput);
|
|
const btn = document.getElementById("btnSearch");
|
|
btn.disabled = true;
|
|
btn.textContent = "查询中...";
|
|
try {
|
|
if (id && /^\d+$/.test(id)) {
|
|
document.getElementById("searchResults").style.display =
|
|
"none";
|
|
await showDetail(id);
|
|
} else {
|
|
renderSearchResults(
|
|
await api(
|
|
"/api/music/search?keyword=" +
|
|
encodeURIComponent(rawInput),
|
|
),
|
|
rawInput,
|
|
);
|
|
}
|
|
} catch (err) {
|
|
showToast("查询失败: " + err.message, "error");
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = "查询";
|
|
}
|
|
}
|
|
|
|
async function downloadMusic() {
|
|
if (!musicInfo || !selectedId) return;
|
|
const btn = document.getElementById("btnDownload");
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-sm"></span> 下载中';
|
|
try {
|
|
const r = await api("/api/music/download", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
id: selectedId,
|
|
name: musicInfo.name,
|
|
artist: musicInfo.artist,
|
|
albumImage: musicInfo.albumImage,
|
|
}),
|
|
});
|
|
musicIdInput.value = "";
|
|
musicInfo = null;
|
|
selectedId = null;
|
|
document.getElementById("musicPreview").style.display =
|
|
"none";
|
|
document.getElementById("searchResults").style.display =
|
|
"none";
|
|
document.getElementById("searchTips").style.display =
|
|
"block";
|
|
parseHint.classList.remove("visible");
|
|
loadResources();
|
|
showToast("已下载: " + r.file, "success");
|
|
} catch (err) {
|
|
showToast("下载失败: " + err.message, "error");
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML =
|
|
'<i class="bi bi-download"></i> 下载到资源库';
|
|
}
|
|
}
|
|
|
|
// ==================== Event Listeners ====================
|
|
document
|
|
.getElementById("btnSearch")
|
|
.addEventListener("click", searchMusic);
|
|
document
|
|
.getElementById("btnDownload")
|
|
.addEventListener("click", downloadMusic);
|
|
document
|
|
.getElementById("btnClear")
|
|
.addEventListener("click", clearPlaylist);
|
|
musicIdInput.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") searchMusic();
|
|
});
|
|
musicPlayer.addEventListener("error", () =>
|
|
showToast("试听失败:该歌曲可能无版权", "error"),
|
|
);
|
|
document
|
|
.getElementById("resourceSearch")
|
|
.addEventListener("input", (e) => {
|
|
resourceKeyword = e.target.value.trim().toLowerCase();
|
|
applyResourceFilter();
|
|
});
|
|
|
|
// ==================== Keyboard Shortcuts ====================
|
|
document.addEventListener("keydown", (e) => {
|
|
if (
|
|
e.target.tagName === "INPUT" ||
|
|
e.target.tagName === "TEXTAREA"
|
|
)
|
|
return;
|
|
if (e.code === "Space") {
|
|
e.preventDefault();
|
|
if (musicPlayer.paused) musicPlayer.play();
|
|
else musicPlayer.pause();
|
|
}
|
|
if (e.code === "ArrowRight")
|
|
musicPlayer.currentTime = Math.min(
|
|
musicPlayer.duration || 0,
|
|
musicPlayer.currentTime + 5,
|
|
);
|
|
if (e.code === "ArrowLeft")
|
|
musicPlayer.currentTime = Math.max(
|
|
0,
|
|
musicPlayer.currentTime - 5,
|
|
);
|
|
});
|
|
|
|
// ==================== Swipe Navigation ====================
|
|
let touchStartX = 0,
|
|
touchStartY = 0,
|
|
touchStartTime = 0;
|
|
const pageOrder = [
|
|
"pageSearch",
|
|
"pageResources",
|
|
"pagePlaylist",
|
|
"pageAbout",
|
|
];
|
|
document.addEventListener(
|
|
"touchstart",
|
|
(e) => {
|
|
touchStartX = e.touches[0].clientX;
|
|
touchStartY = e.touches[0].clientY;
|
|
touchStartTime = Date.now();
|
|
},
|
|
{ passive: true },
|
|
);
|
|
document.addEventListener(
|
|
"touchend",
|
|
(e) => {
|
|
const dx = e.changedTouches[0].clientX - touchStartX;
|
|
const dy = e.changedTouches[0].clientY - touchStartY;
|
|
const dt = Date.now() - touchStartTime;
|
|
if (
|
|
Math.abs(dx) < 60 ||
|
|
Math.abs(dx) < Math.abs(dy) * 1.5 ||
|
|
dt > 500
|
|
)
|
|
return;
|
|
const idx = pageOrder.findIndex((id) =>
|
|
document
|
|
.getElementById(id)
|
|
?.classList.contains("active"),
|
|
);
|
|
if (idx < 0) return;
|
|
if (dx < 0 && idx < pageOrder.length - 1)
|
|
switchPage(pageOrder[idx + 1]);
|
|
else if (dx > 0 && idx > 0) switchPage(pageOrder[idx - 1]);
|
|
},
|
|
{ passive: true },
|
|
);
|
|
|
|
// ==================== Init ====================
|
|
loadResources();
|
|
loadPlaylist();
|
|
loadCacheInfo();
|
|
setInterval(() => {
|
|
loadResources();
|
|
loadPlaylist();
|
|
loadCacheInfo();
|
|
}, 5000);
|