const state = { library: null, currentFolder: "", currentQueue: [], currentTrackIndex: -1, shuffle: false, loopMode: "list", }; const audio = document.getElementById("audioPlayer"); async function fetchLibrary() { const response = await fetch("/api/library"); state.library = await response.json(); renderTree(); renderPlaylists(); if (!state.currentQueue.length) { state.currentQueue = [...state.library.all_tracks]; renderQueue(); } } function toggleButton(id, active, label) { const button = document.getElementById(id); button.classList.toggle("active", active); button.textContent = label; } function folderTracks(path, node = state.library.tree) { if (node.path === path) return flattenNodeTracks(node); for (const child of node.folders) { const result = folderTracks(path, child); if (result) return result; } return null; } function flattenNodeTracks(node) { const tracks = [...node.tracks]; for (const child of node.folders) { tracks.push(...flattenNodeTracks(child)); } return tracks; } function renderTree() { const container = document.getElementById("folderTree"); container.innerHTML = ""; container.appendChild(renderFolderNode(state.library.tree)); } function renderFolderNode(node) { const wrapper = document.createElement("div"); wrapper.className = "folder-node"; const title = document.createElement(node.path ? "h4" : "h3"); title.textContent = node.name; wrapper.appendChild(title); const actions = document.createElement("div"); actions.className = "folder-actions"; const useBtn = document.createElement("button"); useBtn.className = "ghost-btn"; useBtn.textContent = "设为当前目录"; useBtn.onclick = () => { state.currentFolder = node.path; document.getElementById("folderIndicator").textContent = `当前目录:${node.path || "根目录"}`; }; const playBtn = document.createElement("button"); playBtn.className = "ghost-btn"; playBtn.textContent = "播放此目录"; playBtn.onclick = () => { const tracks = flattenNodeTracks(node); if (!tracks.length) return alert("该目录下没有可播放文件"); startQueue(tracks, 0); state.currentFolder = node.path; document.getElementById("folderIndicator").textContent = `当前目录:${node.path || "根目录"}`; }; actions.append(useBtn, playBtn); wrapper.appendChild(actions); node.tracks.forEach((track) => wrapper.appendChild(renderTrack(track))); node.folders.forEach((folder) => wrapper.appendChild(renderFolderNode(folder))); return wrapper; } function renderTrack(track) { const template = document.getElementById("trackItemTemplate"); const fragment = template.content.cloneNode(true); fragment.querySelector(".track-name").textContent = track.name; fragment.querySelector(".track-path").textContent = track.path; fragment.querySelector(".play-btn").onclick = () => startQueue([track], 0); fragment.querySelector(".queue-btn").onclick = () => { state.currentQueue.push(track); renderQueue(); }; fragment.querySelector(".move-btn").onclick = async () => { const destination = prompt("移动到哪个目录?输入相对 mp3file 的目录路径", track.folder || ""); if (destination === null) return; await fetch("/api/move", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ source: track.path, destination_dir: destination }), }); await fetchLibrary(); }; return fragment; } function renderQueue() { const container = document.getElementById("queueList"); container.innerHTML = ""; state.currentQueue.forEach((track, index) => { const item = document.createElement("div"); item.className = "track-item"; if (index === state.currentTrackIndex) item.style.borderColor = "var(--accent)"; item.innerHTML = `
${track.name}

${track.path}

`; const [playBtn, removeBtn] = item.querySelectorAll("button"); playBtn.onclick = () => playTrack(index); removeBtn.onclick = () => { state.currentQueue.splice(index, 1); if (state.currentTrackIndex >= state.currentQueue.length) { state.currentTrackIndex = state.currentQueue.length - 1; } renderQueue(); }; container.appendChild(item); }); } function renderPlaylists() { const container = document.getElementById("playlistList"); container.innerHTML = ""; state.library.playlists.forEach((playlist) => { const item = document.createElement("div"); item.className = "playlist-item"; item.innerHTML = `
${playlist.name}

${playlist.tracks.length} 首歌曲

`; const [playBtn, replaceBtn, deleteBtn] = item.querySelectorAll("button"); playBtn.onclick = () => { const tracks = playlist.tracks .map((path) => state.library.all_tracks.find((track) => track.path === path)) .filter(Boolean); startQueue(tracks, 0); }; replaceBtn.onclick = async () => { await fetch(`/api/playlists/${playlist.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tracks: state.currentQueue.map((track) => track.path) }), }); await fetchLibrary(); }; deleteBtn.onclick = async () => { await fetch(`/api/playlists/${playlist.id}`, { method: "DELETE" }); await fetchLibrary(); }; container.appendChild(item); }); } function startQueue(tracks, index) { state.currentQueue = [...tracks]; state.currentTrackIndex = index; renderQueue(); playTrack(index); } function playTrack(index) { if (!state.currentQueue.length) return; state.currentTrackIndex = index; const track = state.currentQueue[index]; audio.src = track.url; audio.play(); document.getElementById("nowTitle").textContent = track.name; document.getElementById("nowMeta").textContent = track.path; renderQueue(); } function nextTrack() { if (!state.currentQueue.length) return; if (state.loopMode === "one") { playTrack(state.currentTrackIndex); return; } if (state.shuffle) { const randomIndex = Math.floor(Math.random() * state.currentQueue.length); playTrack(randomIndex); return; } const nextIndex = state.currentTrackIndex + 1; if (nextIndex < state.currentQueue.length) { playTrack(nextIndex); return; } if (state.loopMode === "list") { playTrack(0); } } audio.addEventListener("ended", nextTrack); document.getElementById("playAllBtn").onclick = () => startQueue(state.library.all_tracks, 0); document.getElementById("playFolderBtn").onclick = () => { const tracks = folderTracks(state.currentFolder || "") || []; if (!tracks.length) return alert("当前目录没有歌曲"); startQueue(tracks, 0); }; document.getElementById("shuffleBtn").onclick = () => { state.shuffle = !state.shuffle; toggleButton("shuffleBtn", state.shuffle, `随机: ${state.shuffle ? "开" : "关"}`); }; document.getElementById("loopBtn").onclick = () => { state.loopMode = state.loopMode === "list" ? "one" : state.loopMode === "one" ? "off" : "list"; const labels = { list: "循环: 列表", one: "循环: 单曲", off: "循环: 关" }; toggleButton("loopBtn", state.loopMode !== "off", labels[state.loopMode]); }; document.getElementById("uploadBtn").onclick = async () => { const files = document.getElementById("uploadInput").files; if (!files.length) return alert("请选择文件"); const formData = new FormData(); Array.from(files).forEach((file) => formData.append("files", file)); formData.append("target_dir", document.getElementById("targetDir").value.trim()); await fetch("/api/upload", { method: "POST", body: formData }); document.getElementById("uploadInput").value = ""; await fetchLibrary(); }; document.getElementById("createFolderBtn").onclick = async () => { const path = document.getElementById("newFolderInput").value.trim(); if (!path) return alert("请输入目录名"); await fetch("/api/folder", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path }), }); document.getElementById("newFolderInput").value = ""; await fetchLibrary(); }; document.getElementById("createPlaylistBtn").onclick = async () => { const name = document.getElementById("playlistNameInput").value.trim(); if (!name) return alert("请输入播放列表名称"); await fetch("/api/playlists", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, tracks: state.currentQueue.map((track) => track.path) }), }); document.getElementById("playlistNameInput").value = ""; await fetchLibrary(); }; document.getElementById("saveQueueBtn").onclick = () => renderQueue(); fetchLibrary();