|
|
@@ -3,8 +3,7 @@ const state = {
|
|
|
currentFolder: "",
|
|
|
currentQueue: [],
|
|
|
currentTrackIndex: -1,
|
|
|
- shuffle: false,
|
|
|
- loopMode: "list",
|
|
|
+ playMode: "list",
|
|
|
albums: [],
|
|
|
currentView: "home",
|
|
|
activeAlbumPath: "",
|
|
|
@@ -15,6 +14,12 @@ const progressBar = document.getElementById("progressBar");
|
|
|
const audioCacheName = "musicweb-audio-cache-v1";
|
|
|
let toastTimer = 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 = {
|
|
|
@@ -22,8 +27,7 @@ function savePlaybackState() {
|
|
|
currentQueue: state.currentQueue.map((track) => track.path),
|
|
|
currentTrackPath: currentTrack ? currentTrack.path : "",
|
|
|
currentTime: audio.currentTime || 0,
|
|
|
- shuffle: state.shuffle,
|
|
|
- loopMode: state.loopMode,
|
|
|
+ playMode: state.playMode,
|
|
|
activeAlbumPath: state.activeAlbumPath,
|
|
|
};
|
|
|
localStorage.setItem("musicwebplayback", JSON.stringify(payload));
|
|
|
@@ -43,8 +47,7 @@ function restorePlaybackState() {
|
|
|
|
|
|
try {
|
|
|
const saved = JSON.parse(raw);
|
|
|
- state.shuffle = Boolean(saved.shuffle);
|
|
|
- state.loopMode = saved.loopMode || "list";
|
|
|
+ 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();
|
|
|
@@ -162,6 +165,10 @@ 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}" />`
|
|
|
@@ -183,6 +190,7 @@ function renderHome() {
|
|
|
renderAlbumGrid(state.albums);
|
|
|
renderLatestList(latestTracks);
|
|
|
renderRankingList(rankingAlbums);
|
|
|
+ renderAlbumMenu();
|
|
|
renderQueue();
|
|
|
}
|
|
|
|
|
|
@@ -327,7 +335,7 @@ function renderRankingList(items) {
|
|
|
<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="播放"></button>
|
|
|
+ <button class="icon-btn small-icon-btn play-btn" aria-label="播放">${iconMarkup.play}</button>
|
|
|
</div>
|
|
|
`;
|
|
|
card.onclick = () => renderAlbumDetail(album.path);
|
|
|
@@ -387,8 +395,8 @@ function renderQueue() {
|
|
|
<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="播放"></button>
|
|
|
- <button class="icon-btn small-icon-btn move-btn" aria-label="移除"></button>
|
|
|
+ <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");
|
|
|
@@ -426,9 +434,9 @@ function renderPlaylists() {
|
|
|
<p>${playlist.tracks.length} 首歌曲</p>
|
|
|
</div>
|
|
|
<div class="track-actions compact-actions">
|
|
|
- <button class="icon-btn small-icon-btn play-btn" aria-label="播放"></button>
|
|
|
- <button class="icon-btn small-icon-btn queue-btn" aria-label="覆盖"></button>
|
|
|
- <button class="icon-btn small-icon-btn move-btn" aria-label="删除"></button>
|
|
|
+ <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>
|
|
|
`;
|
|
|
|
|
|
@@ -458,13 +466,63 @@ function renderPlaylists() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+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);
|
|
|
+ startQueue(album.tracks, 0);
|
|
|
+ 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() {
|
|
|
- document.getElementById("shuffleBtn").classList.toggle("active", state.shuffle);
|
|
|
- document.getElementById("loopBtn").classList.toggle("active", state.loopMode !== "off");
|
|
|
- document.getElementById("loopBtn").title = `循环: ${state.loopMode === "list" ? "列表" : state.loopMode === "one" ? "单曲" : "关"}`;
|
|
|
- document.getElementById("shuffleBtn").title = `随机: ${state.shuffle ? "开" : "关"}`;
|
|
|
- document.getElementById("loopBtn").dataset.tip = state.loopMode === "list" ? "列表循环" : state.loopMode === "one" ? "单曲循环" : "循环关闭";
|
|
|
- document.getElementById("shuffleBtn").dataset.tip = state.shuffle ? "随机播放: 开" : "随机播放: 关";
|
|
|
+ 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) {
|
|
|
@@ -546,7 +604,7 @@ function playPrevious() {
|
|
|
|
|
|
function playNext() {
|
|
|
if (!state.currentQueue.length) return;
|
|
|
- if (state.shuffle) {
|
|
|
+ if (state.playMode === "shuffle") {
|
|
|
playTrack(Math.floor(Math.random() * state.currentQueue.length));
|
|
|
return;
|
|
|
}
|
|
|
@@ -556,11 +614,11 @@ function playNext() {
|
|
|
|
|
|
function nextTrack() {
|
|
|
if (!state.currentQueue.length) return;
|
|
|
- if (state.loopMode === "one") {
|
|
|
+ if (state.playMode === "one") {
|
|
|
playTrack(state.currentTrackIndex);
|
|
|
return;
|
|
|
}
|
|
|
- if (state.loopMode === "off" && state.currentTrackIndex === state.currentQueue.length - 1) {
|
|
|
+ if (state.playMode === "once" || state.currentTrackIndex === state.currentQueue.length - 1) {
|
|
|
audio.pause();
|
|
|
syncPlayButton();
|
|
|
return;
|
|
|
@@ -596,17 +654,22 @@ progressBar.addEventListener("input", () => {
|
|
|
|
|
|
document.getElementById("backHomeBtn").onclick = () => showView("home");
|
|
|
document.getElementById("saveQueueBtn").onclick = () => renderQueue();
|
|
|
-document.getElementById("shuffleBtn").onclick = () => {
|
|
|
- state.shuffle = !state.shuffle;
|
|
|
+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("shuffleBtn").dataset.tip);
|
|
|
+ showIconToast(document.getElementById("playModeBtn").dataset.tip);
|
|
|
savePlaybackState();
|
|
|
};
|
|
|
-document.getElementById("loopBtn").onclick = () => {
|
|
|
- state.loopMode = state.loopMode === "list" ? "one" : state.loopMode === "one" ? "off" : "list";
|
|
|
- updateToggleStates();
|
|
|
- showIconToast(document.getElementById("loopBtn").dataset.tip);
|
|
|
- savePlaybackState();
|
|
|
+document.getElementById("albumMenuBtn").onclick = () => toggleAlbumMenu();
|
|
|
+document.getElementById("closeAlbumMenuBtn").onclick = () => toggleAlbumMenu(false);
|
|
|
+document.getElementById("albumMenuHomeBtn").onclick = () => {
|
|
|
+ toggleAlbumMenu(false);
|
|
|
+ showView("home");
|
|
|
+};
|
|
|
+document.getElementById("albumMenuOverlay").onclick = (event) => {
|
|
|
+ if (event.target.id === "albumMenuOverlay") toggleAlbumMenu(false);
|
|
|
};
|
|
|
document.getElementById("prevBtn").onclick = () => {
|
|
|
showIconToast("上一曲");
|