Переглянути джерело

更新全部风格,按钮等,上下一曲可用

sequoia00 3 тижнів тому
батько
коміт
dbec60f2d2
5 змінених файлів з 80 додано та 37 видалено
  1. 0 0
      AGENTS.md
  2. 6 1
      app/main.py
  3. 1 1
      start.sh
  4. 41 27
      static/css/style.css
  5. 32 8
      static/js/app.js

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
AGENTS.md


+ 6 - 1
app/main.py

@@ -175,7 +175,12 @@ def stream_file(file_path: str) -> FileResponse:
     if not file.exists() or not file.is_file():
         raise HTTPException(status_code=404, detail="File not found")
     media_type = mimetypes.guess_type(file.name)[0] or "application/octet-stream"
-    return FileResponse(file, media_type=media_type, filename=file.name)
+    return FileResponse(
+        file,
+        media_type=media_type,
+        filename=file.name,
+        headers={"Cache-Control": "private, max-age=31536000, immutable"},
+    )
 
 
 @app.get("/api/cover/{file_path:path}")

+ 1 - 1
start.sh

@@ -1,2 +1,2 @@
 #!/bin/bash
-nohup uvicorn app.main:app --host 0.0.0.0 --port 8006 &
+nohup uvicorn app.main:app --host 0.0.0.0 --port 8006 --reload &

+ 41 - 27
static/css/style.css

@@ -1,13 +1,13 @@
 :root {
-  --bg: #f6efe6;
-  --panel: rgba(255, 249, 242, 0.94);
-  --panel-strong: #fffaf5;
-  --line: rgba(95, 73, 45, 0.12);
-  --text: #2c2218;
-  --muted: #7b6854;
-  --accent: #d76434;
-  --accent-deep: #b6481c;
-  --shadow: 0 18px 50px rgba(88, 55, 26, 0.14);
+  --bg: #eaf4ff;
+  --panel: rgba(247, 251, 255, 0.93);
+  --panel-strong: rgba(255, 255, 255, 0.95);
+  --line: rgba(28, 93, 153, 0.16);
+  --text: #10243a;
+  --muted: #58718b;
+  --accent: #2388e8;
+  --accent-deep: #0e63b6;
+  --shadow: 0 18px 50px rgba(9, 50, 94, 0.18);
 }
 
 * {
@@ -20,9 +20,12 @@ body {
   font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
   color: var(--text);
   background:
-    radial-gradient(circle at top left, rgba(215, 100, 52, 0.18), transparent 28%),
-    radial-gradient(circle at top right, rgba(255, 200, 127, 0.18), transparent 22%),
-    linear-gradient(180deg, #faf3e8 0%, #f4ebde 45%, #efe4d5 100%);
+    linear-gradient(rgba(231, 244, 255, 0.78), rgba(205, 229, 250, 0.9)),
+    url("/static/img/background.jpg"),
+    linear-gradient(160deg, #eaf6ff 0%, #b9dcfb 48%, #74b4f1 100%);
+  background-attachment: fixed;
+  background-position: center;
+  background-size: cover;
 }
 
 button,
@@ -97,7 +100,7 @@ input[type="file"] {
   flex: 0 0 auto;
   padding: 7px 10px;
   border-radius: 999px;
-  background: rgba(215, 100, 52, 0.1);
+  background: rgba(35, 136, 232, 0.12);
   color: var(--accent-deep);
   font-size: 0.82rem;
 }
@@ -222,8 +225,8 @@ input[type="file"] {
 .detail-cover-wrap {
   position: relative;
   overflow: hidden;
-  background: linear-gradient(145deg, rgba(215, 100, 52, 0.94), rgba(169, 65, 29, 0.92));
-  color: #fff7f0;
+  background: linear-gradient(145deg, rgba(35, 136, 232, 0.95), rgba(14, 99, 182, 0.94));
+  color: #f7fbff;
   display: flex;
   align-items: center;
   justify-content: center;
@@ -290,6 +293,17 @@ input[type="file"] {
   gap: 8px;
 }
 
+.album-card .card-actions {
+  flex-wrap: nowrap;
+}
+
+.album-card .card-actions button {
+  flex: 1 1 0;
+  min-width: 0;
+  padding: 7px 8px;
+  font-size: 0.78rem;
+}
+
 .compact-actions {
   flex-wrap: nowrap;
 }
@@ -303,12 +317,12 @@ input[type="file"] {
 }
 
 .primary-btn {
-  background: linear-gradient(135deg, var(--accent), #ea9660);
-  color: #fff7f1;
+  background: linear-gradient(135deg, var(--accent), #5db8ff);
+  color: #f7fbff;
 }
 
 .secondary-btn {
-  background: rgba(215, 100, 52, 0.1);
+  background: rgba(35, 136, 232, 0.12);
   color: var(--accent-deep);
 }
 
@@ -347,8 +361,8 @@ input[type="file"] {
 }
 
 .queue-list .song-row.is-active {
-  border-color: rgba(215, 100, 52, 0.5);
-  box-shadow: inset 0 0 0 1px rgba(215, 100, 52, 0.15);
+  border-color: rgba(35, 136, 232, 0.5);
+  box-shadow: inset 0 0 0 1px rgba(35, 136, 232, 0.16);
 }
 
 .rank-badge {
@@ -359,7 +373,7 @@ input[type="file"] {
   align-items: center;
   justify-content: center;
   border-radius: 14px;
-  background: rgba(215, 100, 52, 0.12);
+  background: rgba(35, 136, 232, 0.12);
   color: var(--accent-deep);
   font-weight: 700;
 }
@@ -407,7 +421,7 @@ input[type="file"] {
   padding: 12px 14px;
   border-radius: 24px;
   background:
-    linear-gradient(180deg, rgba(255, 250, 244, 0.97), rgba(246, 237, 226, 0.99)),
+    linear-gradient(180deg, rgba(247, 251, 255, 0.97), rgba(231, 243, 255, 0.99)),
     var(--panel);
 }
 
@@ -439,8 +453,8 @@ input[type="file"] {
   display: inline-flex;
   align-items: center;
   justify-content: center;
-  background: linear-gradient(135deg, #de8552, #b54c22);
-  color: #fff8f2;
+  background: linear-gradient(135deg, #45a6f5, #0e63b6);
+  color: #f7fbff;
   font-size: 1rem;
   font-weight: 700;
 }
@@ -499,7 +513,7 @@ input[type="range"] {
 }
 
 .icon-btn.active {
-  background: rgba(215, 100, 52, 0.12);
+  background: rgba(35, 136, 232, 0.12);
   color: var(--accent-deep);
 }
 
@@ -507,8 +521,8 @@ input[type="range"] {
   width: 54px;
   height: 54px;
   flex-basis: 54px;
-  background: linear-gradient(135deg, var(--accent), #e98f58);
-  color: #fff8f2;
+  background: linear-gradient(135deg, var(--accent), #5db8ff);
+  color: #f7fbff;
 }
 
 .small-icon-btn {

+ 32 - 8
static/js/app.js

@@ -12,6 +12,7 @@ const state = {
 
 const audio = document.getElementById("audioPlayer");
 const progressBar = document.getElementById("progressBar");
+const audioCacheName = "musicweb-audio-cache-v1";
 let toastTimer = null;
 
 function savePlaybackState() {
@@ -166,9 +167,9 @@ function buildCoverMarkup(album, className) {
     ? `<img class="cover-image" src="${album.cover}" alt="${album.name}" />`
     : "";
   return `
-    <div class="${className}">
+    <div class="${className}${album.cover ? " has-cover" : ""}">
       ${imageMarkup}
-      <span class="cover-name">${coverLabel(album.name)}</span>
+      ${album.cover ? "" : `<span class="cover-name">${coverLabel(album.name)}</span>`}
     </div>
   `;
 }
@@ -295,7 +296,7 @@ function renderAlbumDetail(path) {
 
   const container = document.getElementById("detailTrackList");
   container.innerHTML = "";
-  album.tracks.forEach((track) => container.appendChild(renderTrack(track)));
+  album.tracks.forEach((track, index) => container.appendChild(renderTrack(track, album.tracks, index)));
 }
 
 function renderLatestList(tracks) {
@@ -305,7 +306,7 @@ function renderLatestList(tracks) {
     container.innerHTML = '<div class="empty-state">暂无最新更新</div>';
     return;
   }
-  tracks.forEach((track) => container.appendChild(renderTrack(track)));
+  tracks.forEach((track, index) => container.appendChild(renderTrack(track, tracks, index)));
 }
 
 function renderRankingList(items) {
@@ -340,14 +341,14 @@ function renderRankingList(items) {
   });
 }
 
-function renderTrack(track) {
+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([track], 0);
+    startQueue(sourceTracks, sourceIndex);
   };
   fragment.querySelector(".queue-btn").onclick = () => {
     showIconToast("加入队列");
@@ -503,11 +504,34 @@ function syncPlayButton() {
   document.getElementById("playToggleBtn").dataset.tip = audio.paused || audio.ended ? "播放" : "暂停";
 }
 
-function playTrack(index) {
+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 = track.url;
+  audio.src = await getCachedAudioUrl(track);
   audio.play();
   updateDock(track);
   renderQueue();

Деякі файли не було показано, через те що забагато файлів було змінено