app.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. const state = {
  2. library: null,
  3. currentFolder: "",
  4. currentQueue: [],
  5. currentTrackIndex: -1,
  6. shuffle: false,
  7. loopMode: "list",
  8. };
  9. const audio = document.getElementById("audioPlayer");
  10. async function fetchLibrary() {
  11. const response = await fetch("/api/library");
  12. state.library = await response.json();
  13. renderTree();
  14. renderPlaylists();
  15. if (!state.currentQueue.length) {
  16. state.currentQueue = [...state.library.all_tracks];
  17. renderQueue();
  18. }
  19. }
  20. function toggleButton(id, active, label) {
  21. const button = document.getElementById(id);
  22. button.classList.toggle("active", active);
  23. button.textContent = label;
  24. }
  25. function folderTracks(path, node = state.library.tree) {
  26. if (node.path === path) return flattenNodeTracks(node);
  27. for (const child of node.folders) {
  28. const result = folderTracks(path, child);
  29. if (result) return result;
  30. }
  31. return null;
  32. }
  33. function flattenNodeTracks(node) {
  34. const tracks = [...node.tracks];
  35. for (const child of node.folders) {
  36. tracks.push(...flattenNodeTracks(child));
  37. }
  38. return tracks;
  39. }
  40. function renderTree() {
  41. const container = document.getElementById("folderTree");
  42. container.innerHTML = "";
  43. container.appendChild(renderFolderNode(state.library.tree));
  44. }
  45. function renderFolderNode(node) {
  46. const wrapper = document.createElement("div");
  47. wrapper.className = "folder-node";
  48. const title = document.createElement(node.path ? "h4" : "h3");
  49. title.textContent = node.name;
  50. wrapper.appendChild(title);
  51. const actions = document.createElement("div");
  52. actions.className = "folder-actions";
  53. const useBtn = document.createElement("button");
  54. useBtn.className = "ghost-btn";
  55. useBtn.textContent = "设为当前目录";
  56. useBtn.onclick = () => {
  57. state.currentFolder = node.path;
  58. document.getElementById("folderIndicator").textContent = `当前目录:${node.path || "根目录"}`;
  59. };
  60. const playBtn = document.createElement("button");
  61. playBtn.className = "ghost-btn";
  62. playBtn.textContent = "播放此目录";
  63. playBtn.onclick = () => {
  64. const tracks = flattenNodeTracks(node);
  65. if (!tracks.length) return alert("该目录下没有可播放文件");
  66. startQueue(tracks, 0);
  67. state.currentFolder = node.path;
  68. document.getElementById("folderIndicator").textContent = `当前目录:${node.path || "根目录"}`;
  69. };
  70. actions.append(useBtn, playBtn);
  71. wrapper.appendChild(actions);
  72. node.tracks.forEach((track) => wrapper.appendChild(renderTrack(track)));
  73. node.folders.forEach((folder) => wrapper.appendChild(renderFolderNode(folder)));
  74. return wrapper;
  75. }
  76. function renderTrack(track) {
  77. const template = document.getElementById("trackItemTemplate");
  78. const fragment = template.content.cloneNode(true);
  79. fragment.querySelector(".track-name").textContent = track.name;
  80. fragment.querySelector(".track-path").textContent = track.path;
  81. fragment.querySelector(".play-btn").onclick = () => startQueue([track], 0);
  82. fragment.querySelector(".queue-btn").onclick = () => {
  83. state.currentQueue.push(track);
  84. renderQueue();
  85. };
  86. fragment.querySelector(".move-btn").onclick = async () => {
  87. const destination = prompt("移动到哪个目录?输入相对 mp3file 的目录路径", track.folder || "");
  88. if (destination === null) return;
  89. await fetch("/api/move", {
  90. method: "POST",
  91. headers: { "Content-Type": "application/json" },
  92. body: JSON.stringify({ source: track.path, destination_dir: destination }),
  93. });
  94. await fetchLibrary();
  95. };
  96. return fragment;
  97. }
  98. function renderQueue() {
  99. const container = document.getElementById("queueList");
  100. container.innerHTML = "";
  101. state.currentQueue.forEach((track, index) => {
  102. const item = document.createElement("div");
  103. item.className = "track-item";
  104. if (index === state.currentTrackIndex) item.style.borderColor = "var(--accent)";
  105. item.innerHTML = `
  106. <div>
  107. <strong class="track-name">${track.name}</strong>
  108. <p class="track-path">${track.path}</p>
  109. </div>
  110. <div class="track-actions">
  111. <button class="ghost-btn">播放</button>
  112. <button class="ghost-btn">移除</button>
  113. </div>
  114. `;
  115. const [playBtn, removeBtn] = item.querySelectorAll("button");
  116. playBtn.onclick = () => playTrack(index);
  117. removeBtn.onclick = () => {
  118. state.currentQueue.splice(index, 1);
  119. if (state.currentTrackIndex >= state.currentQueue.length) {
  120. state.currentTrackIndex = state.currentQueue.length - 1;
  121. }
  122. renderQueue();
  123. };
  124. container.appendChild(item);
  125. });
  126. }
  127. function renderPlaylists() {
  128. const container = document.getElementById("playlistList");
  129. container.innerHTML = "";
  130. state.library.playlists.forEach((playlist) => {
  131. const item = document.createElement("div");
  132. item.className = "playlist-item";
  133. item.innerHTML = `
  134. <div>
  135. <strong>${playlist.name}</strong>
  136. <p>${playlist.tracks.length} 首歌曲</p>
  137. </div>
  138. <div class="track-actions">
  139. <button class="ghost-btn">播放</button>
  140. <button class="ghost-btn">覆盖</button>
  141. <button class="ghost-btn">删除</button>
  142. </div>
  143. `;
  144. const [playBtn, replaceBtn, deleteBtn] = item.querySelectorAll("button");
  145. playBtn.onclick = () => {
  146. const tracks = playlist.tracks
  147. .map((path) => state.library.all_tracks.find((track) => track.path === path))
  148. .filter(Boolean);
  149. startQueue(tracks, 0);
  150. };
  151. replaceBtn.onclick = async () => {
  152. await fetch(`/api/playlists/${playlist.id}`, {
  153. method: "PUT",
  154. headers: { "Content-Type": "application/json" },
  155. body: JSON.stringify({ tracks: state.currentQueue.map((track) => track.path) }),
  156. });
  157. await fetchLibrary();
  158. };
  159. deleteBtn.onclick = async () => {
  160. await fetch(`/api/playlists/${playlist.id}`, { method: "DELETE" });
  161. await fetchLibrary();
  162. };
  163. container.appendChild(item);
  164. });
  165. }
  166. function startQueue(tracks, index) {
  167. state.currentQueue = [...tracks];
  168. state.currentTrackIndex = index;
  169. renderQueue();
  170. playTrack(index);
  171. }
  172. function playTrack(index) {
  173. if (!state.currentQueue.length) return;
  174. state.currentTrackIndex = index;
  175. const track = state.currentQueue[index];
  176. audio.src = track.url;
  177. audio.play();
  178. document.getElementById("nowTitle").textContent = track.name;
  179. document.getElementById("nowMeta").textContent = track.path;
  180. renderQueue();
  181. }
  182. function nextTrack() {
  183. if (!state.currentQueue.length) return;
  184. if (state.loopMode === "one") {
  185. playTrack(state.currentTrackIndex);
  186. return;
  187. }
  188. if (state.shuffle) {
  189. const randomIndex = Math.floor(Math.random() * state.currentQueue.length);
  190. playTrack(randomIndex);
  191. return;
  192. }
  193. const nextIndex = state.currentTrackIndex + 1;
  194. if (nextIndex < state.currentQueue.length) {
  195. playTrack(nextIndex);
  196. return;
  197. }
  198. if (state.loopMode === "list") {
  199. playTrack(0);
  200. }
  201. }
  202. audio.addEventListener("ended", nextTrack);
  203. document.getElementById("playAllBtn").onclick = () => startQueue(state.library.all_tracks, 0);
  204. document.getElementById("playFolderBtn").onclick = () => {
  205. const tracks = folderTracks(state.currentFolder || "") || [];
  206. if (!tracks.length) return alert("当前目录没有歌曲");
  207. startQueue(tracks, 0);
  208. };
  209. document.getElementById("shuffleBtn").onclick = () => {
  210. state.shuffle = !state.shuffle;
  211. toggleButton("shuffleBtn", state.shuffle, `随机: ${state.shuffle ? "开" : "关"}`);
  212. };
  213. document.getElementById("loopBtn").onclick = () => {
  214. state.loopMode = state.loopMode === "list" ? "one" : state.loopMode === "one" ? "off" : "list";
  215. const labels = { list: "循环: 列表", one: "循环: 单曲", off: "循环: 关" };
  216. toggleButton("loopBtn", state.loopMode !== "off", labels[state.loopMode]);
  217. };
  218. document.getElementById("uploadBtn").onclick = async () => {
  219. const files = document.getElementById("uploadInput").files;
  220. if (!files.length) return alert("请选择文件");
  221. const formData = new FormData();
  222. Array.from(files).forEach((file) => formData.append("files", file));
  223. formData.append("target_dir", document.getElementById("targetDir").value.trim());
  224. await fetch("/api/upload", { method: "POST", body: formData });
  225. document.getElementById("uploadInput").value = "";
  226. await fetchLibrary();
  227. };
  228. document.getElementById("createFolderBtn").onclick = async () => {
  229. const path = document.getElementById("newFolderInput").value.trim();
  230. if (!path) return alert("请输入目录名");
  231. await fetch("/api/folder", {
  232. method: "POST",
  233. headers: { "Content-Type": "application/json" },
  234. body: JSON.stringify({ path }),
  235. });
  236. document.getElementById("newFolderInput").value = "";
  237. await fetchLibrary();
  238. };
  239. document.getElementById("createPlaylistBtn").onclick = async () => {
  240. const name = document.getElementById("playlistNameInput").value.trim();
  241. if (!name) return alert("请输入播放列表名称");
  242. await fetch("/api/playlists", {
  243. method: "POST",
  244. headers: { "Content-Type": "application/json" },
  245. body: JSON.stringify({ name, tracks: state.currentQueue.map((track) => track.path) }),
  246. });
  247. document.getElementById("playlistNameInput").value = "";
  248. await fetchLibrary();
  249. };
  250. document.getElementById("saveQueueBtn").onclick = () => renderQueue();
  251. fetchLibrary();