follow: the artist page's heart becomes a real thing, stored and celebrated
Following an artist now writes a row: followed_artists (artist_id, name, portrait, followed_at) beside the playlists in the library db, reached through follow_artist / unfollow_artist / followed_artists, each emitting soulseek:follows-changed. Re-following refreshes the name and portrait — identity improves as pages are visited and a later portrait is usually the better one — but keeps the original followed_at, so "following since" stays honest. Name and portrait are stored on purpose rather than looked up: the home page's "new releases by artists you follow" can then draw a card from the row alone, with no catalog request per artist. The store is the UI truth and the chip never keeps its own copy. It is optimistic — the chip flips at the click and reverts if the write fails, rather than leaving the screen telling a lie. The chip itself (FB4) leads the stat row and wears the accent as ground with dark text, the Play pill's own trick, so its on-state is legible across the room. The lead slot is a fixed 14x14 in which the +, the equalizer bars and the check swap places, so nothing about the chip's box moves except the word — and the word morphs between its measured widths instead of snapping. Clicking runs the Encore: the + collapses, three bars dance three beats with a note rising per beat, they bow, and the check springs in as "Following" grows into place. The db write and the toast fire at the CLICK; the ceremony is presentation only, so navigating away mid-show still follows. Unfollow is an undo — the shrink morphs, nothing celebrates, and the toast says so. Every effect is template-owned with scoped keyframes. The first build spawned the glow and the notes imperatively and referenced -global- keyframes, which silently did not resolve: the glow ran no animation and sat at full opacity, the notes held opacity 0 and never appeared. The bars worked throughout for exactly the reason the others did not. The row around it is shorter too, at Kyle's call: the library and listening chips (progress meter, the disk popover, format, hours, last played, top track, genre) come out, leaving the identity trio and the socials. With nothing left to shed, the socials' scroll-driven fade and their layout drop at the park boundary come out with them, and the divider's clear space moves from the follow heart by name to whichever chip now closes the run. The JS that fed the removed chips stays — it is cheap, local, and the numbers still have a home coming.
This commit is contained in:
1 parent
5786269592
commit
520e2ad4e4
8 files changed
+763
-197
No files matched your search
@@ -58,6 +58,17 @@ pub struct PlaylistTrack {
|
||||
pub art_path: Option<String>,
|
||||
}
|
||||
|
||||
/// One followed artist. Name and portrait are stored alongside the id so the
|
||||
/// future home page can render "artists you follow" without paying a catalog
|
||||
/// request per artist — the row alone is enough to draw a card.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FollowedArtist {
|
||||
pub artist_id: u64,
|
||||
pub name: String,
|
||||
pub portrait: Option<String>,
|
||||
pub followed_at: i64,
|
||||
}
|
||||
|
||||
/// Cumulative listening stats for one artist (drives the artist-page chips).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PlayStats {
|
||||
@@ -161,6 +172,12 @@ impl Library {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_plays_artist ON plays(artist COLLATE NOCASE);
|
||||
CREATE INDEX IF NOT EXISTS idx_plays_started ON plays(started_at DESC);
|
||||
CREATE TABLE IF NOT EXISTS followed_artists (
|
||||
artist_id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
portrait TEXT,
|
||||
followed_at INTEGER NOT NULL
|
||||
);
|
||||
",
|
||||
)?;
|
||||
// Migrate databases created before artist portraits were added.
|
||||
@@ -612,6 +629,56 @@ impl Library {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---- followed artists ----
|
||||
|
||||
/// Idempotent: re-following refreshes the stored name/portrait (identity
|
||||
/// improves over time — a page visit can carry a better portrait) but
|
||||
/// keeps the original followed_at, so "following since" stays honest.
|
||||
pub fn follow_artist(
|
||||
&self,
|
||||
artist_id: u64,
|
||||
name: &str,
|
||||
portrait: Option<&str>,
|
||||
now: i64,
|
||||
) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO followed_artists (artist_id, name, portrait, followed_at)
|
||||
VALUES (?1, ?2, ?3, ?4)
|
||||
ON CONFLICT(artist_id)
|
||||
DO UPDATE SET name = ?2, portrait = COALESCE(?3, portrait)",
|
||||
rusqlite::params![artist_id as i64, name, portrait, now],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unfollow_artist(&self, artist_id: u64) -> Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"DELETE FROM followed_artists WHERE artist_id = ?1",
|
||||
[artist_id as i64],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Newest follow first — the order a home-page rail wants.
|
||||
pub fn followed_artists(&self) -> Result<Vec<FollowedArtist>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT artist_id, name, portrait, followed_at
|
||||
FROM followed_artists ORDER BY followed_at DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |r| {
|
||||
Ok(FollowedArtist {
|
||||
artist_id: r.get::<_, i64>(0)? as u64,
|
||||
name: r.get(1)?,
|
||||
portrait: r.get(2)?,
|
||||
followed_at: r.get(3)?,
|
||||
})
|
||||
})?;
|
||||
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||
}
|
||||
|
||||
// ---- playlists ----
|
||||
|
||||
pub fn create_playlist(&self, name: &str, now: i64) -> Result<i64> {
|
||||
|
||||
Reference in new issue
Block a user