Browse Source

修改底部播放按钮,功能聚合

sequoia00 3 weeks ago
parent
commit
2f1d3de8dd

BIN
img/anniu.jpg


+ 0 - 0
img/01.jpg → img/other/01.jpg


+ 0 - 0
img/02.jpg → img/other/02.jpg


BIN
img/other/Screenshot_20260506_160629_com_tencent_qqmusic_NewPlayerActivity.jpg


+ 0 - 0
img/chenglong.jpg → img/other/chenglong.jpg


+ 0 - 0
img/mowenwei.jpg → img/other/mowenwei.jpg


+ 109 - 12
static/css/style.css

@@ -380,21 +380,40 @@ input[type="file"] {
 
 .detail-header {
   display: grid;
-  grid-template-columns: 1fr;
-  gap: 14px;
-  padding: 14px;
+  grid-template-columns: minmax(0, 1fr) 88px;
+  align-items: center;
+  gap: 10px;
+  padding: 10px;
 }
 
 .detail-cover-wrap {
-  width: 100%;
+  width: 88px;
   aspect-ratio: 1 / 1;
-  border-radius: 22px;
+  border-radius: 16px;
 }
 
 .detail-copy {
   display: flex;
   flex-direction: column;
-  gap: 10px;
+  gap: 8px;
+  min-width: 0;
+}
+
+.detail-copy .card-actions {
+  gap: 6px;
+}
+
+.detail-copy .card-actions button {
+  flex: 1 1 calc(50% - 3px);
+  padding: 7px 9px;
+  font-size: 0.78rem;
+}
+
+.detail-copy .card-actions .detail-back-home {
+  flex-basis: 100%;
+  background: rgba(35, 136, 232, 0.18);
+  color: var(--accent-deep);
+  font-weight: 700;
 }
 
 .field {
@@ -541,6 +560,17 @@ input[type="range"] {
   stroke-linejoin: round;
 }
 
+.icon-btn svg text {
+  fill: currentColor;
+  font-size: 8px;
+  font-weight: 700;
+  stroke: none;
+}
+
+.icon-btn svg .thick-icon-path {
+  stroke-width: 2.6;
+}
+
 .small-icon-btn svg {
   width: 16px;
   height: 16px;
@@ -579,6 +609,78 @@ audio {
   display: none;
 }
 
+.album-menu-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: 1100;
+  background: rgba(8, 31, 55, 0.2);
+  display: flex;
+  align-items: flex-end;
+  justify-content: center;
+  padding: 12px;
+}
+
+.album-menu {
+  width: min(460px, 100%);
+  max-height: 52vh;
+  overflow: hidden;
+  border: 1px solid var(--line);
+  border-radius: 18px;
+  background: rgba(247, 251, 255, 0.98);
+  box-shadow: var(--shadow);
+}
+
+.album-menu-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+  padding: 10px 12px;
+  border-bottom: 1px solid var(--line);
+}
+
+.album-menu-actions {
+  display: flex;
+  gap: 8px;
+}
+
+.album-menu-actions button {
+  padding: 7px 10px;
+  font-size: 0.78rem;
+}
+
+.album-menu-list {
+  max-height: calc(52vh - 54px);
+  overflow-y: auto;
+  padding: 8px;
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 8px;
+}
+
+.album-menu-item {
+  min-width: 0;
+  padding: 9px 10px;
+  border-radius: 12px;
+  background: rgba(35, 136, 232, 0.1);
+  color: var(--text);
+  text-align: left;
+}
+
+.album-menu-item span,
+.album-menu-item small {
+  display: block;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.album-menu-item small {
+  margin-top: 2px;
+  color: var(--muted);
+  font-size: 0.72rem;
+}
+
 @media (min-width: 760px) {
   .app-shell {
     padding: 12px 14px 184px;
@@ -594,12 +696,7 @@ audio {
     gap: 10px;
   }
 
-  .detail-header {
-    grid-template-columns: 120px 1fr;
-    align-items: start;
-  }
-
   .detail-cover-wrap {
-    width: 120px;
+    width: 96px;
   }
 }

BIN
static/img/background.jpg


+ 92 - 29
static/js/app.js

@@ -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("上一曲");

+ 24 - 9
templates/index.html

@@ -73,11 +73,6 @@
 
         <section id="detailView" class="view-stack is-hidden">
           <section class="content-section detail-header">
-            <button id="backHomeBtn" class="back-btn">返回首页</button>
-            <div class="detail-cover-wrap">
-              <img id="detailCoverImage" class="detail-cover-image is-hidden" alt="cover" />
-              <div id="detailCoverFallback" class="detail-cover-fallback">专辑</div>
-            </div>
             <div class="detail-copy">
               <p class="eyebrow">Album Detail</p>
               <h2 id="detailTitle">专辑详情</h2>
@@ -85,8 +80,13 @@
               <div class="card-actions">
                 <button id="detailPlayBtn" class="primary-btn">播放当前目录</button>
                 <button id="detailPlayAllBtn" class="secondary-btn">加入并播放</button>
+                <button id="backHomeBtn" class="back-btn detail-back-home">返回首页</button>
               </div>
             </div>
+            <div class="detail-cover-wrap">
+              <img id="detailCoverImage" class="detail-cover-image is-hidden" alt="cover" />
+              <div id="detailCoverFallback" class="detail-cover-fallback">专辑</div>
+            </div>
           </section>
 
           <section class="content-section">
@@ -160,8 +160,8 @@
         </div>
 
         <div class="player-actions icon-actions">
-          <button id="shuffleBtn" class="icon-btn" aria-label="随机" data-tip="随机播放">
-            <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 4h4v4M20 4l-5 5M4 7h5l6 6M4 17h5l2-2M15 15l5 5M20 20v-4"/></svg>
+          <button id="playModeBtn" class="icon-btn" aria-label="播放模式" data-tip="列表播放">
+            <svg id="playModeIcon" viewBox="0 0 24 24" aria-hidden="true"></svg>
           </button>
           <button id="prevBtn" class="icon-btn" aria-label="上一曲" data-tip="上一曲">
             <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5v14M18 6l-8 6 8 6V6z"/></svg>
@@ -173,13 +173,28 @@
           <button id="nextBtn" class="icon-btn" aria-label="下一曲" data-tip="下一曲">
             <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 5v14M6 6l8 6-8 6V6z"/></svg>
           </button>
-          <button id="loopBtn" class="icon-btn" aria-label="循环" data-tip="列表循环">
-            <svg viewBox="0 0 24 24" aria-hidden="true"><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"/></svg>
+          <button id="albumMenuBtn" class="icon-btn" aria-label="专辑列表" data-tip="专辑列表">
+            <svg viewBox="0 0 24 24" aria-hidden="true">
+              <path d="M4 7h16M4 12h16M4 17h16"/>
+              <path d="M16 14l3 3-3 3"/>
+            </svg>
           </button>
         </div>
 
         <audio id="audioPlayer" preload="metadata"></audio>
       </div>
+      <div id="albumMenuOverlay" class="album-menu-overlay is-hidden">
+        <div class="album-menu">
+          <div class="album-menu-head">
+            <strong>选择专辑</strong>
+            <div class="album-menu-actions">
+              <button id="closeAlbumMenuBtn" class="text-btn">关闭</button>
+              <button id="albumMenuHomeBtn" class="back-btn">返回首页</button>
+            </div>
+          </div>
+          <div id="albumMenuList" class="album-menu-list"></div>
+        </div>
+      </div>
       <div id="iconToast" class="icon-toast is-hidden"></div>
     </div>