| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- 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 = `
- <div>
- <strong class="track-name">${track.name}</strong>
- <p class="track-path">${track.path}</p>
- </div>
- <div class="track-actions">
- <button class="ghost-btn">播放</button>
- <button class="ghost-btn">移除</button>
- </div>
- `;
- 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 = `
- <div>
- <strong>${playlist.name}</strong>
- <p>${playlist.tracks.length} 首歌曲</p>
- </div>
- <div class="track-actions">
- <button class="ghost-btn">播放</button>
- <button class="ghost-btn">覆盖</button>
- <button class="ghost-btn">删除</button>
- </div>
- `;
- 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();
|