| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814 |
- const state = {
- library: null,
- currentFolder: "",
- currentQueue: [],
- currentTrackIndex: -1,
- playMode: "list",
- albums: [],
- currentView: "home",
- activeAlbumPath: "",
- featuredAlbums: [],
- featuredIndex: 0,
- };
- const audio = document.getElementById("audioPlayer");
- const progressBar = document.getElementById("progressBar");
- const audioCacheName = "musicweb-audio-cache-v1";
- let toastTimer = null;
- let featuredTimer = null;
- const iconMarkup = {
- play: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7-11-7z"/></svg>',
- replace: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h10M4 12h10M4 17h7M18 9v8M14 13h8"/></svg>',
- remove: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 7h12M9 7V5h6v2M9 10v8M15 10v8M7 7l1 14h8l1-14"/></svg>',
- };
- function savePlaybackState() {
- const currentTrack = state.currentQueue[state.currentTrackIndex] || null;
- const payload = {
- currentFolder: state.currentFolder,
- currentQueue: state.currentQueue.map((track) => track.path),
- currentTrackPath: currentTrack ? currentTrack.path : "",
- currentTime: audio.currentTime || 0,
- playMode: state.playMode,
- activeAlbumPath: state.activeAlbumPath,
- };
- localStorage.setItem("musicwebplayback", JSON.stringify(payload));
- }
- function showIconToast(text) {
- const toast = document.getElementById("iconToast");
- toast.textContent = text;
- toast.classList.remove("is-hidden");
- if (toastTimer) clearTimeout(toastTimer);
- toastTimer = setTimeout(() => toast.classList.add("is-hidden"), 1200);
- }
- function restorePlaybackState() {
- const raw = localStorage.getItem("musicwebplayback");
- if (!raw || !state.library) return;
- try {
- const saved = JSON.parse(raw);
- state.playMode = saved.playMode || (saved.shuffle ? "shuffle" : saved.loopMode === "one" ? "one" : saved.loopMode === "off" ? "once" : "list");
- state.currentFolder = saved.currentFolder || "";
- state.activeAlbumPath = saved.activeAlbumPath || "";
- updateToggleStates();
- setCurrentFolder(state.currentFolder);
- if (Array.isArray(saved.currentQueue) && saved.currentQueue.length) {
- state.currentQueue = saved.currentQueue
- .map((path) => state.library.all_tracks.find((track) => track.path === path))
- .filter(Boolean);
- }
- const trackPath = saved.currentTrackPath || "";
- if (trackPath) {
- const queueIndex = state.currentQueue.findIndex((track) => track.path === trackPath);
- if (queueIndex >= 0) {
- state.currentTrackIndex = queueIndex;
- } else {
- const single = state.library.all_tracks.find((track) => track.path === trackPath);
- if (single) {
- state.currentQueue = [single];
- state.currentTrackIndex = 0;
- }
- }
- }
- if (state.currentQueue.length && state.currentTrackIndex >= 0) {
- const track = state.currentQueue[state.currentTrackIndex];
- audio.src = track.url;
- updateDock(track);
- renderQueue();
- syncPlayButton();
- audio.addEventListener(
- "loadedmetadata",
- () => {
- audio.currentTime = Number(saved.currentTime || 0);
- },
- { once: true }
- );
- }
- if (state.activeAlbumPath) {
- renderAlbumDetail(state.activeAlbumPath);
- }
- } catch {
- localStorage.removeItem("musicwebplayback");
- }
- }
- async function fetchLibrary() {
- const response = await fetch("/api/library");
- state.library = await response.json();
- state.albums = buildAlbums(state.library.tree);
- state.featuredAlbums = buildFeaturedAlbums(state.albums);
- state.featuredIndex = 0;
- document.getElementById("albumCount").textContent = state.albums.length;
- document.getElementById("trackCount").textContent = state.library.all_tracks.length;
- renderHome();
- renderPlaylists();
- restorePlaybackState();
- if (state.activeAlbumPath) {
- renderAlbumDetail(state.activeAlbumPath);
- }
- startFeaturedCarousel();
- }
- function buildAlbums(tree) {
- const albums = [];
- function visit(node, ancestors = []) {
- const tracks = flattenNodeTracks(node);
- const childAlbums = node.folders.filter((child) => flattenNodeTracks(child).length);
- if (node.path && tracks.length && !childAlbums.length) {
- const nameParts = [...ancestors, node.name].filter(Boolean);
- albums.push({
- name: nameParts.join("") || "未命名专辑",
- path: node.path,
- cover: node.cover || null,
- tracks,
- });
- }
- const nextAncestors = node.path ? [...ancestors, node.name] : ancestors;
- node.folders.forEach((child) => visit(child, nextAncestors));
- }
- visit(tree);
- return albums;
- }
- function flattenNodeTracks(node) {
- const tracks = [...node.tracks];
- node.folders.forEach((child) => tracks.push(...flattenNodeTracks(child)));
- return tracks;
- }
- function folderTracks(path) {
- const album = state.albums.find((item) => item.path === path);
- return album ? [...album.tracks] : [];
- }
- function setCurrentFolder(path) {
- state.currentFolder = path;
- document.getElementById("folderIndicator").textContent = path || "根目录";
- }
- function showView(name) {
- state.currentView = name;
- document.getElementById("homeView").classList.toggle("is-hidden", name !== "home");
- document.getElementById("detailView").classList.toggle("is-hidden", name !== "detail");
- savePlaybackState();
- }
- function scrollToPageTop() {
- requestAnimationFrame(() => {
- const scroller = document.scrollingElement || document.documentElement;
- scroller.scrollTop = 0;
- scroller.scrollLeft = 0;
- window.scrollTo(0, 0);
- });
- }
- function coverLabel(name) {
- return name.length > 12 ? `${name.slice(0, 12)}...` : name;
- }
- function shortAlbumName(name) {
- return name.length > 8 ? `${name.slice(0, 8)}...` : name;
- }
- function buildCoverMarkup(album, className) {
- const imageMarkup = album.cover
- ? `<img class="cover-image" src="${album.cover}" alt="${album.name}" />`
- : "";
- return `
- <div class="${className}${album.cover ? " has-cover" : ""}">
- ${imageMarkup}
- ${album.cover ? "" : `<span class="cover-name">${coverLabel(album.name)}</span>`}
- </div>
- `;
- }
- function renderHome() {
- const latestTracks = [...state.library.all_tracks].slice(-8).reverse();
- const rankingAlbums = [...state.albums].sort((a, b) => b.tracks.length - a.tracks.length).slice(0, 6);
- renderFeaturedAlbums(state.featuredAlbums);
- renderAlbumGrid(state.albums);
- renderLatestList(latestTracks);
- renderRankingList(rankingAlbums);
- renderAlbumMenu();
- renderQueue();
- }
- function buildAlbumCardActions(card, album) {
- const detailBtn = card.querySelector("[data-action='detail']");
- const playBtn = card.querySelector("[data-action='play']");
- detailBtn.onclick = () => renderAlbumDetail(album.path);
- playBtn.onclick = (event) => {
- event.stopPropagation();
- setCurrentFolder(album.path);
- startQueue(album.tracks, 0);
- };
- card.onclick = () => renderAlbumDetail(album.path);
- }
- function renderFeaturedAlbums(items) {
- const container = document.getElementById("featuredAlbums");
- container.innerHTML = "";
- if (!items.length) {
- container.innerHTML = '<div class="empty-state">暂无可展示的专辑目录</div>';
- return;
- }
- const album = items[state.featuredIndex] || items[0];
- container.appendChild(buildFeaturedAlbumCard(album));
- }
- function buildFeaturedAlbums(albums) {
- const sorted = [...albums].sort((a, b) => b.tracks.length - a.tracks.length);
- if (sorted.length <= 5) return sorted;
- return sorted.slice(0, 8);
- }
- function startFeaturedCarousel() {
- if (featuredTimer) clearInterval(featuredTimer);
- if (state.featuredAlbums.length <= 1) return;
- featuredTimer = setInterval(() => {
- state.featuredIndex = (state.featuredIndex + 1) % state.featuredAlbums.length;
- renderFeaturedAlbums(state.featuredAlbums);
- }, 3000);
- }
- function buildFeaturedAlbumCard(album) {
- const card = document.createElement("article");
- card.className = "feature-card";
- card.innerHTML = `
- <div class="feature-card-inner">
- ${buildCoverMarkup(album, "feature-cover")}
- <div class="feature-body">
- <div>
- <h3>${album.name}</h3>
- <p class="meta-row">${album.tracks.length} 首歌曲 · ${album.path}</p>
- </div>
- <p class="subtext">点击进入详细目录页,查看歌曲列表并播放。</p>
- <div class="card-actions">
- <button class="primary-btn" data-action="detail">进入目录</button>
- <button class="secondary-btn" data-action="play">播放</button>
- </div>
- </div>
- </div>
- `;
- buildAlbumCardActions(card, album);
- return card;
- }
- function renderAlbumGrid(items) {
- const container = document.getElementById("albumGrid");
- container.innerHTML = "";
- if (!items.length) {
- container.innerHTML = '<div class="empty-state">当前音乐库还没有专辑目录</div>';
- return;
- }
- items.forEach((album) => {
- const card = document.createElement("article");
- card.className = "album-card";
- card.innerHTML = `
- ${buildCoverMarkup(album, "album-cover")}
- <div class="album-body">
- <div>
- <h3>${album.name}</h3>
- <p class="meta-row">${album.tracks.length} 首歌曲</p>
- </div>
- <div class="card-actions">
- <button class="secondary-btn" data-action="detail">进入</button>
- <button class="text-btn" data-action="play">播放</button>
- </div>
- </div>
- `;
- buildAlbumCardActions(card, album);
- container.appendChild(card);
- });
- }
- function renderAlbumDetail(path) {
- const album = state.albums.find((item) => item.path === path);
- if (!album) return;
- state.activeAlbumPath = path;
- setCurrentFolder(path);
- showView("detail");
- scrollToPageTop();
- document.getElementById("detailTitle").textContent = album.name;
- document.getElementById("detailMeta").textContent = `${album.path} · ${album.tracks.length} 首歌曲`;
- document.getElementById("detailTrackCount").textContent = `${album.tracks.length} 首`;
- const coverImage = document.getElementById("detailCoverImage");
- const coverFallback = document.getElementById("detailCoverFallback");
- if (album.cover) {
- coverImage.src = album.cover;
- coverImage.classList.remove("is-hidden");
- coverFallback.classList.add("is-hidden");
- } else {
- coverImage.removeAttribute("src");
- coverImage.classList.add("is-hidden");
- coverFallback.classList.remove("is-hidden");
- coverFallback.textContent = coverLabel(album.name);
- }
- document.getElementById("detailPlayBtn").onclick = () => startQueue(album.tracks, 0);
- document.getElementById("detailPlayAllBtn").onclick = () => {
- state.currentQueue = [...album.tracks, ...state.currentQueue];
- state.currentTrackIndex = 0;
- renderQueue();
- playTrack(0);
- };
- const container = document.getElementById("detailTrackList");
- container.innerHTML = "";
- album.tracks.forEach((track, index) => container.appendChild(renderTrack(track, album.tracks, index)));
- }
- function renderLatestList(tracks) {
- const container = document.getElementById("latestList");
- container.innerHTML = "";
- if (!tracks.length) {
- container.innerHTML = '<div class="empty-state">暂无最新更新</div>';
- return;
- }
- tracks.forEach((track, index) => container.appendChild(renderTrack(track, tracks, index)));
- }
- function renderRankingList(items) {
- const container = document.getElementById("rankingList");
- container.innerHTML = "";
- if (!items.length) {
- container.innerHTML = '<div class="empty-state">暂无排名数据</div>';
- return;
- }
- items.forEach((album, index) => {
- const card = document.createElement("div");
- card.className = "ranking-card";
- card.innerHTML = `
- <span class="rank-badge">${index + 1}</span>
- <div class="ranking-main">
- <h3>${album.name}</h3>
- <p class="ranking-note">${album.tracks.length} 首歌曲</p>
- </div>
- <div class="track-actions compact-actions">
- <button class="icon-btn small-icon-btn play-btn" aria-label="播放">${iconMarkup.play}</button>
- </div>
- `;
- card.onclick = () => renderAlbumDetail(album.path);
- card.querySelector("button").onclick = (event) => {
- event.stopPropagation();
- showIconToast("播放");
- setCurrentFolder(album.path);
- startQueue(album.tracks, 0);
- };
- container.appendChild(card);
- });
- }
- function renderTrack(track, sourceTracks = [track], sourceIndex = 0) {
- 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 = () => {
- showIconToast("播放");
- startQueue(sourceTracks, sourceIndex);
- };
- fragment.querySelector(".queue-btn").onclick = () => {
- showIconToast("加入队列");
- state.currentQueue.push(track);
- renderQueue();
- savePlaybackState();
- };
- fragment.querySelector(".move-btn").onclick = async () => {
- showIconToast("移动歌曲");
- 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 = "";
- if (!state.currentQueue.length) {
- container.innerHTML = '<div class="empty-state">当前播放队列为空</div>';
- return;
- }
- state.currentQueue.forEach((track, index) => {
- const row = document.createElement("div");
- row.className = `song-row${index === state.currentTrackIndex ? " is-active" : ""}`;
- row.innerHTML = `
- <div class="song-copy">
- <strong class="track-name">${track.name}</strong>
- <p class="track-path">${track.path}</p>
- </div>
- <div class="track-actions compact-actions">
- <button class="icon-btn small-icon-btn play-btn" aria-label="播放">${iconMarkup.play}</button>
- <button class="icon-btn small-icon-btn move-btn" aria-label="移除">${iconMarkup.remove}</button>
- </div>
- `;
- const [playBtn, removeBtn] = row.querySelectorAll("button");
- playBtn.onclick = () => {
- showIconToast("播放");
- playTrack(index);
- };
- removeBtn.onclick = () => {
- showIconToast("移除");
- state.currentQueue.splice(index, 1);
- if (state.currentTrackIndex >= state.currentQueue.length) {
- state.currentTrackIndex = state.currentQueue.length - 1;
- }
- renderQueue();
- savePlaybackState();
- };
- container.appendChild(row);
- });
- }
- function clearQueue() {
- state.currentQueue = [];
- clearCurrentTrack();
- }
- function renderPlaylists() {
- const container = document.getElementById("playlistList");
- container.innerHTML = "";
- if (!state.library.playlists.length) {
- container.innerHTML = '<div class="empty-state">还没有播放列表</div>';
- return;
- }
- 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 compact-actions">
- <button class="icon-btn small-icon-btn play-btn" aria-label="播放">${iconMarkup.play}</button>
- <button class="icon-btn small-icon-btn queue-btn" aria-label="覆盖">${iconMarkup.replace}</button>
- <button class="icon-btn small-icon-btn move-btn" aria-label="删除">${iconMarkup.remove}</button>
- </div>
- `;
- const [playBtn, replaceBtn, deleteBtn] = item.querySelectorAll("button");
- playBtn.onclick = () => {
- showIconToast("播放列表");
- const tracks = playlist.tracks
- .map((path) => state.library.all_tracks.find((track) => track.path === path))
- .filter(Boolean);
- startQueue(tracks, 0);
- };
- replaceBtn.onclick = async () => {
- showIconToast("覆盖列表");
- 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 () => {
- showIconToast("删除列表");
- await fetch(`/api/playlists/${playlist.id}`, { method: "DELETE" });
- await fetchLibrary();
- };
- container.appendChild(item);
- });
- }
- function renderAlbumMenu() {
- const container = document.getElementById("albumMenuList");
- if (!container) return;
- container.innerHTML = "";
- if (!state.albums.length) {
- container.innerHTML = '<div class="empty-state">暂无专辑</div>';
- return;
- }
- state.albums.forEach((album) => {
- const button = document.createElement("button");
- button.className = "album-menu-item";
- button.innerHTML = `
- <span>${shortAlbumName(album.name)}</span>
- <small>${album.tracks.length} 首</small>
- `;
- button.onclick = () => {
- setCurrentFolder(album.path);
- renderAlbumDetail(album.path);
- toggleAlbumMenu(false);
- };
- container.appendChild(button);
- });
- }
- function toggleAlbumMenu(forceOpen) {
- const overlay = document.getElementById("albumMenuOverlay");
- const open = typeof forceOpen === "boolean" ? forceOpen : overlay.classList.contains("is-hidden");
- overlay.classList.toggle("is-hidden", !open);
- }
- function updateToggleStates() {
- const modes = {
- shuffle: {
- label: "随机播放",
- icon: '<path d="M16 4h4v4M20 4l-5 5M4 7h5l6 6M4 17h5l2-2M15 15l5 5M20 20v-4"/>',
- },
- one: {
- label: "单曲循环",
- icon: '<path d="M17 2l4 4-4 4M3 11V9a3 3 0 0 1 3-3h15M7 22l-4-4 4-4M21 13v2a3 3 0 0 1-3 3H3"/><text x="12" y="16" text-anchor="middle">1</text>',
- },
- list: {
- label: "列表播放",
- icon: '<path d="M17 2l4 4-4 4M3 11V9a3 3 0 0 1 3-3h15M7 22l-4-4 4-4M21 13v2a3 3 0 0 1-3 3H3"/>',
- },
- once: {
- label: "单次播放",
- icon: '<path class="thick-icon-path" d="M5 12h13M14 8l4 4-4 4"/>',
- },
- };
- const mode = modes[state.playMode] || modes.list;
- const button = document.getElementById("playModeBtn");
- const icon = document.getElementById("playModeIcon");
- button.dataset.tip = mode.label;
- button.title = mode.label;
- icon.innerHTML = mode.icon;
- }
- function startQueue(tracks, index) {
- if (!tracks.length) return;
- state.currentQueue = [...tracks];
- state.currentTrackIndex = index;
- renderQueue();
- playTrack(index);
- savePlaybackState();
- }
- function updateDock(track) {
- document.getElementById("nowTitle").textContent = track.name;
- document.getElementById("nowMeta").textContent = track.path;
- document.getElementById("dockTitle").textContent = track.name;
- document.getElementById("dockPath").textContent = track.path;
- const coverImg = document.getElementById("dockCoverImage");
- const fallback = document.getElementById("dockCoverFallback");
- const album = state.albums.find((item) => item.path === track.folder) || null;
- if (album && album.cover) {
- coverImg.src = album.cover;
- coverImg.classList.remove("is-hidden");
- fallback.classList.add("is-hidden");
- } else {
- coverImg.removeAttribute("src");
- coverImg.classList.add("is-hidden");
- fallback.classList.remove("is-hidden");
- fallback.textContent = (track.name || "乐").charAt(0).toUpperCase();
- }
- }
- function clearCurrentTrack() {
- audio.pause();
- audio.removeAttribute("src");
- audio.load();
- state.currentTrackIndex = -1;
- state.activeAlbumPath = "";
- document.getElementById("nowTitle").textContent = "选择一个专辑目录开始播放";
- document.getElementById("nowMeta").textContent = "点击专辑进入详情页,或直接在卡片上快速播放。";
- document.getElementById("dockTitle").textContent = "未开始播放";
- document.getElementById("dockPath").textContent = "点击专辑或歌曲开始播放";
- document.getElementById("currentTimeLabel").textContent = "00:00";
- document.getElementById("durationLabel").textContent = "00:00";
- progressBar.value = 0;
- const coverImg = document.getElementById("dockCoverImage");
- const fallback = document.getElementById("dockCoverFallback");
- coverImg.removeAttribute("src");
- coverImg.classList.add("is-hidden");
- fallback.classList.remove("is-hidden");
- fallback.textContent = "乐";
- renderQueue();
- syncPlayButton();
- savePlaybackState();
- }
- function syncPlayButton() {
- document.getElementById("playToggleBtn").classList.toggle("is-playing", !audio.paused && !audio.ended);
- document.querySelector(".icon-play").classList.toggle("is-hidden", !audio.paused && !audio.ended);
- document.querySelector(".icon-pause").classList.toggle("is-hidden", audio.paused || audio.ended);
- document.getElementById("playToggleBtn").dataset.tip = audio.paused || audio.ended ? "播放" : "暂停";
- }
- async function getCachedAudioUrl(track) {
- if (!("caches" in window)) return track.url;
- try {
- const cache = await caches.open(audioCacheName);
- const cached = await cache.match(track.url);
- if (cached) {
- const blob = await cached.blob();
- return URL.createObjectURL(blob);
- }
- fetch(track.url, { credentials: "same-origin" })
- .then((response) => {
- if (response.ok) cache.put(track.url, response.clone());
- })
- .catch(() => {});
- } catch {
- return track.url;
- }
- return track.url;
- }
- async function playTrack(index) {
- if (!state.currentQueue.length) return;
- state.currentTrackIndex = index;
- const track = state.currentQueue[index];
- audio.src = await getCachedAudioUrl(track);
- audio.play();
- updateDock(track);
- renderQueue();
- savePlaybackState();
- }
- function playPrevious() {
- if (!state.currentQueue.length) return;
- const prevIndex = state.currentTrackIndex > 0 ? state.currentTrackIndex - 1 : state.currentQueue.length - 1;
- playTrack(prevIndex);
- }
- function playNext() {
- if (!state.currentQueue.length) return;
- if (state.playMode === "shuffle") {
- playTrack(Math.floor(Math.random() * state.currentQueue.length));
- return;
- }
- const nextIndex = state.currentTrackIndex + 1 < state.currentQueue.length ? state.currentTrackIndex + 1 : 0;
- playTrack(nextIndex);
- }
- function nextTrack() {
- if (!state.currentQueue.length) return;
- if (state.playMode === "one") {
- playTrack(state.currentTrackIndex);
- return;
- }
- if (state.playMode === "once" || state.currentTrackIndex === state.currentQueue.length - 1) {
- audio.pause();
- syncPlayButton();
- return;
- }
- playNext();
- }
- function formatTime(seconds) {
- if (!Number.isFinite(seconds)) return "00:00";
- const minutes = Math.floor(seconds / 60);
- const secs = Math.floor(seconds % 60);
- return `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
- }
- audio.addEventListener("ended", nextTrack);
- audio.addEventListener("play", syncPlayButton);
- audio.addEventListener("pause", syncPlayButton);
- audio.addEventListener("timeupdate", () => {
- const progress = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0;
- progressBar.value = progress;
- document.getElementById("currentTimeLabel").textContent = formatTime(audio.currentTime);
- document.getElementById("durationLabel").textContent = formatTime(audio.duration);
- savePlaybackState();
- });
- audio.addEventListener("loadedmetadata", () => {
- document.getElementById("durationLabel").textContent = formatTime(audio.duration);
- });
- progressBar.addEventListener("input", () => {
- if (!audio.duration) return;
- audio.currentTime = (Number(progressBar.value) / 100) * audio.duration;
- });
- document.getElementById("backHomeBtn").onclick = () => {
- showView("home");
- scrollToPageTop();
- };
- document.getElementById("saveQueueBtn").onclick = () => renderQueue();
- document.getElementById("clearQueueBtn").onclick = () => {
- if (!state.currentQueue.length) return;
- showIconToast("清空队列");
- clearQueue();
- };
- document.getElementById("playModeBtn").onclick = () => {
- const modes = ["shuffle", "one", "list", "once"];
- const nextIndex = (modes.indexOf(state.playMode) + 1) % modes.length;
- state.playMode = modes[nextIndex];
- updateToggleStates();
- showIconToast(document.getElementById("playModeBtn").dataset.tip);
- savePlaybackState();
- };
- document.getElementById("albumMenuBtn").onclick = () => toggleAlbumMenu();
- document.getElementById("closeAlbumMenuBtn").onclick = () => toggleAlbumMenu(false);
- document.getElementById("albumMenuHomeBtn").onclick = () => {
- toggleAlbumMenu(false);
- showView("home");
- clearCurrentTrack();
- scrollToPageTop();
- };
- document.getElementById("albumMenuOverlay").onclick = (event) => {
- if (event.target.id === "albumMenuOverlay") toggleAlbumMenu(false);
- };
- document.getElementById("prevBtn").onclick = () => {
- showIconToast("上一曲");
- playPrevious();
- };
- document.getElementById("nextBtn").onclick = () => {
- showIconToast("下一曲");
- playNext();
- };
- document.getElementById("playToggleBtn").onclick = () => {
- if (!state.currentQueue.length && state.library?.all_tracks?.length) {
- startQueue(state.library.all_tracks, 0);
- showIconToast("开始播放");
- return;
- }
- if (audio.paused) {
- audio.play();
- showIconToast("播放");
- } else {
- audio.pause();
- showIconToast("暂停");
- }
- };
- document.getElementById("uploadBtn").onclick = async () => {
- const files = document.getElementById("uploadInput").files;
- if (!files.length) {
- alert("请选择文件");
- return;
- }
- 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) {
- alert("请输入目录名");
- return;
- }
- 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) {
- alert("请输入播放列表名称");
- return;
- }
- 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();
- };
- updateToggleStates();
- fetchLibrary();
|