music(player): 收藏功能 — title 旁 ★/☆,收藏的曲目置顶
deploy music / build-and-deploy (push) Successful in 1m58s

This commit is contained in:
Fam Zheng
2026-05-26 10:04:21 +01:00
parent 0756362d14
commit 83418c198f
2 changed files with 66 additions and 5 deletions
+41 -1
View File
@@ -95,7 +95,15 @@
<template v-else> <template v-else>
<header class="now-playing"> <header class="now-playing">
<h2>{{ selected.title }}</h2> <h2>
<button
class="fav-btn"
:class="{ on: selected.favorite }"
:title="selected.favorite ? '取消收藏' : '收藏'"
@click="toggleFavorite"
>{{ selected.favorite ? '★' : '☆' }}</button>
{{ selected.title }}
</h2>
<div class="np-sub"> <div class="np-sub">
<span v-if="selected.artist">{{ selected.artist }}</span> <span v-if="selected.artist">{{ selected.artist }}</span>
<span v-if="selected.category">· {{ selected.category }}</span> <span v-if="selected.category">· {{ selected.category }}</span>
@@ -921,6 +929,22 @@ function setTab(k) {
activeTab.value = k activeTab.value = k
} }
async function toggleFavorite() {
if (!selectedId.value || !selected.value) return
const next = !selected.value.favorite
selected.value.favorite = next // optimistic
const inList = pieces.value.find(p => p.id === selectedId.value)
if (inList) inList.favorite = next
try {
await patchPiece(selectedId.value, { favorite: next })
} catch (e) {
// 回滚
selected.value.favorite = !next
if (inList) inList.favorite = !next
alert(e.message || String(e))
}
}
// notes auto-save // notes auto-save
function onNotesInput() { function onNotesInput() {
if (!selectedId.value) return if (!selectedId.value) return
@@ -1445,7 +1469,23 @@ onBeforeUnmount(() => {
font-size: 22px; font-size: 22px;
color: var(--accent); color: var(--accent);
margin-bottom: 4px; margin-bottom: 4px;
display: inline-flex;
align-items: center;
gap: 8px;
} }
.fav-btn {
font-size: 22px;
line-height: 1;
color: var(--text-mute);
background: none;
border: none;
cursor: pointer;
padding: 0 4px;
transition: color 0.15s, transform 0.05s;
}
.fav-btn:hover { color: #f5b800; }
.fav-btn.on { color: #f5b800; }
.fav-btn:active { transform: scale(0.85); }
.np-sub { .np-sub {
color: var(--text-dim); color: var(--text-dim);
font-size: 13px; font-size: 13px;
+25 -4
View File
@@ -135,6 +135,11 @@ async fn main() -> std::io::Result<()> {
CREATE INDEX IF NOT EXISTS idx_pp_piece ON playlist_pieces(piece_id);", CREATE INDEX IF NOT EXISTS idx_pp_piece ON playlist_pieces(piece_id);",
) )
.expect("init schema"); .expect("init schema");
// 兼容旧 db:增量加 favorite 列
let _ = conn.execute(
"ALTER TABLE pieces ADD COLUMN favorite INTEGER NOT NULL DEFAULT 0",
[],
);
tracing::info!(%db_path, blobs = %blobs_dir.display(), "music ready"); tracing::info!(%db_path, blobs = %blobs_dir.display(), "music ready");
let chord_url = let chord_url =
@@ -216,6 +221,7 @@ struct PieceSummary {
kinds: Vec<String>, kinds: Vec<String>,
tags: Vec<String>, tags: Vec<String>,
has_lyrics: bool, has_lyrics: bool,
favorite: bool,
created_at: String, created_at: String,
} }
@@ -232,6 +238,7 @@ struct PieceDetail {
created_at: String, created_at: String,
attachments: Vec<Attachment>, attachments: Vec<Attachment>,
tags: Vec<String>, tags: Vec<String>,
favorite: bool,
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -271,6 +278,7 @@ struct PatchPiece {
tags: Option<Vec<String>>, tags: Option<Vec<String>>,
/// admin / import 用:直接写 play_countmvp 无认证) /// admin / import 用:直接写 play_countmvp 无认证)
play_count: Option<i64>, play_count: Option<i64>,
favorite: Option<bool>,
} }
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
@@ -312,12 +320,13 @@ async fn list_pieces(
CASE WHEN p.lyrics IS NOT NULL AND length(p.lyrics) > 0 THEN 1 ELSE 0 END AS has_lyrics, CASE WHEN p.lyrics IS NOT NULL AND length(p.lyrics) > 0 THEN 1 ELSE 0 END AS has_lyrics,
COALESCE((SELECT GROUP_CONCAT(t.name, char(9)) COALESCE((SELECT GROUP_CONCAT(t.name, char(9))
FROM piece_tags pt2 JOIN tags t ON t.id = pt2.tag_id FROM piece_tags pt2 JOIN tags t ON t.id = pt2.tag_id
WHERE pt2.piece_id = p.id), '') AS tags WHERE pt2.piece_id = p.id), '') AS tags,
COALESCE(p.favorite, 0) AS favorite
FROM pieces p FROM pieces p
{filter_join} {filter_join}
{filter_where} {filter_where}
GROUP BY p.id GROUP BY p.id
ORDER BY p.title COLLATE NOCASE ASC, p.id ASC" ORDER BY p.favorite DESC, p.title COLLATE NOCASE ASC, p.id ASC"
); );
let mut stmt = conn.prepare(&sql)?; let mut stmt = conn.prepare(&sql)?;
let bind_refs: Vec<&dyn rusqlite::ToSql> = bind.iter().map(|v| v as &dyn rusqlite::ToSql).collect(); let bind_refs: Vec<&dyn rusqlite::ToSql> = bind.iter().map(|v| v as &dyn rusqlite::ToSql).collect();
@@ -336,6 +345,7 @@ async fn list_pieces(
tags_raw.split('\t').map(|x| x.to_string()).collect() tags_raw.split('\t').map(|x| x.to_string()).collect()
}; };
let has_lyrics: i64 = r.get(9)?; let has_lyrics: i64 = r.get(9)?;
let fav: i64 = r.get(11)?;
Ok(PieceSummary { Ok(PieceSummary {
id: r.get(0)?, id: r.get(0)?,
title: r.get(1)?, title: r.get(1)?,
@@ -348,6 +358,7 @@ async fn list_pieces(
kinds, kinds,
tags, tags,
has_lyrics: has_lyrics != 0, has_lyrics: has_lyrics != 0,
favorite: fav != 0,
}) })
})? })?
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
@@ -392,10 +403,12 @@ async fn get_piece(
i64, i64,
Option<String>, Option<String>,
String, String,
i64,
); );
let row: Option<PieceRow> = conn let row: Option<PieceRow> = conn
.query_row( .query_row(
"SELECT title, artist, category, notes, lyrics, play_count, last_played_at, created_at "SELECT title, artist, category, notes, lyrics, play_count, last_played_at, created_at,
COALESCE(favorite, 0)
FROM pieces WHERE id = ?1", FROM pieces WHERE id = ?1",
params![id], params![id],
|r| { |r| {
@@ -408,11 +421,12 @@ async fn get_piece(
r.get(5)?, r.get(5)?,
r.get(6)?, r.get(6)?,
r.get(7)?, r.get(7)?,
r.get(8)?,
)) ))
}, },
) )
.optional()?; .optional()?;
let (title, artist, category, notes, lyrics, play_count, last_played_at, created_at) = let (title, artist, category, notes, lyrics, play_count, last_played_at, created_at, favorite) =
row.ok_or(AppError::NotFound)?; row.ok_or(AppError::NotFound)?;
let mut stmt = conn.prepare( let mut stmt = conn.prepare(
@@ -456,6 +470,7 @@ async fn get_piece(
created_at, created_at,
attachments, attachments,
tags, tags,
favorite: favorite != 0,
})) }))
} }
@@ -516,6 +531,12 @@ async fn patch_piece(
params![pc, id], params![pc, id],
)?; )?;
} }
if let Some(fav) = body.favorite {
conn.execute(
"UPDATE pieces SET favorite = ?1 WHERE id = ?2",
params![fav as i64, id],
)?;
}
if let Some(tags) = body.tags { if let Some(tags) = body.tags {
conn.execute( conn.execute(
"DELETE FROM piece_tags WHERE piece_id = ?1", "DELETE FROM piece_tags WHERE piece_id = ?1",