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();