feat(web): My Wave button, mood picker and endless wave playback (M4)
- WaveButton: TypeScript port of the prototype's canvas animation (orb and liquid), breathing at rest, pulsing to the track's BPM and energy in the cover palette, following the cursor, pressing with a spring; reduced motion and hidden tabs slow or stop it - home: the wave in the centre with activity and mood chips; changing them while playing rebuilds the upcoming tracks without stopping - player wave mode: asks for the next batch when two tracks are left, sends complete / skip / early skip / like / dislike as feedback and refreshes the queue right after a reaction or an early skip - tracks expose bpm, energy and loudness once analysed; library page shows analysis progress and 7-day wave metrics; wave style setting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3908814cda
commit
eaedd1fc62
24 files changed
+1193
-244
No files matched your search
@@ -38,6 +38,11 @@ type TrackRow struct {
|
||||
Album AlbumRow
|
||||
Genres []string
|
||||
Available bool
|
||||
// Признаки анализа; Analyzed = false — трек ещё не проанализирован.
|
||||
Analyzed bool
|
||||
BPM *float32
|
||||
Energy *float32
|
||||
LoudnessLUFS *float32
|
||||
}
|
||||
|
||||
// ArtistRow — исполнитель со счётчиками.
|
||||
@@ -58,8 +63,10 @@ const trackSelect = `
|
||||
COALESCE((SELECT array_agg(g.name ORDER BY g.name) FROM track_genres tg JOIN genres g ON g.id = tg.genre_id
|
||||
WHERE tg.track_id = t.id), '{}'),
|
||||
EXISTS (SELECT 1 FROM media_files f JOIN library_sources s ON s.id = f.source_id
|
||||
WHERE f.id = t.primary_file_id AND s.status = 'mounted')
|
||||
WHERE f.id = t.primary_file_id AND s.status = 'mounted'),
|
||||
tf.track_id IS NOT NULL, tf.bpm, tf.energy, tf.loudness_lufs
|
||||
FROM tracks t
|
||||
LEFT JOIN track_features tf ON tf.track_id = t.id
|
||||
JOIN albums al ON al.id = t.album_id
|
||||
JOIN artists aa ON aa.id = al.album_artist_id`
|
||||
|
||||
@@ -70,7 +77,8 @@ func scanTrack(row pgx.Row) (TrackRow, error) {
|
||||
)
|
||||
err := row.Scan(&t.ID, &t.Title, &t.DurationMs, &t.DiscNo, &t.TrackNo, &artists,
|
||||
&t.Album.ID, &t.Album.Title, &t.Album.Year, &t.Album.CoverPath, &t.Album.CoverColors,
|
||||
&t.Album.Artist.ID, &t.Album.Artist.Name, &t.Genres, &t.Available)
|
||||
&t.Album.Artist.ID, &t.Album.Artist.Name, &t.Genres, &t.Available, &t.Analyzed, &t.BPM, &t.Energy,
|
||||
&t.LoudnessLUFS)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return TrackRow{}, ErrNotFound
|
||||
}
|
||||
|
||||
@@ -362,10 +362,14 @@ func (d DTO) Track(t TrackRow) gen.Track {
|
||||
if genres == nil {
|
||||
genres = []string{}
|
||||
}
|
||||
return gen.Track{
|
||||
out := gen.Track{
|
||||
Id: t.ID, Title: t.Title, DurationMs: t.DurationMs, Artists: artists, Album: d.albumRef(t.Album),
|
||||
Genres: genres, DiscNo: t.DiscNo, TrackNo: t.TrackNo, Available: t.Available,
|
||||
}
|
||||
if t.Analyzed {
|
||||
out.Features = &gen.TrackFeatures{Bpm: t.BPM, Energy: t.Energy, LoudnessLufs: t.LoudnessLUFS}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AlbumSummary переводит альбом в схему AlbumSummary.
|
||||
|
||||
Reference in new issue
Block a user