Make the now-playing bar's title/artist clickable and add a cover art lightbox
The song title links to its album, the artist name links to the artist page, and clicking the cover art opens a larger version in a modal. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014r98AtcBbevpscWLsfwVvv
This commit is contained in:
@@ -492,6 +492,21 @@ func ListSongArtistNames(db *sql.DB, songID string) ([]string, error) {
|
|||||||
return out, rows.Err()
|
return out, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrimarySongArtistID returns the ID of a song's first-credited artist
|
||||||
|
// (position 0), or "" if the song has no linked artist — used to link
|
||||||
|
// the now-playing bar's artist name to that artist's page.
|
||||||
|
func PrimarySongArtistID(db *sql.DB, songID string) (string, error) {
|
||||||
|
var id string
|
||||||
|
err := db.QueryRow(`SELECT a.id FROM song_artists sa JOIN artists a ON a.id = sa.artist_id WHERE sa.song_id = ? ORDER BY sa.position LIMIT 1`, songID).Scan(&id)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
func nullInt(n sql.NullInt64) any {
|
func nullInt(n sql.NullInt64) any {
|
||||||
if !n.Valid {
|
if !n.Valid {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type SongRow struct {
|
|||||||
ID string
|
ID string
|
||||||
Title string
|
Title string
|
||||||
ArtistName string
|
ArtistName string
|
||||||
|
ArtistID string
|
||||||
AlbumTitle string
|
AlbumTitle string
|
||||||
AlbumID string
|
AlbumID string
|
||||||
CoverArtID string
|
CoverArtID string
|
||||||
@@ -81,6 +82,9 @@ func toSongRow(db *sql.DB, s *database.Song, album *database.Album, userID strin
|
|||||||
row.ArtistName += ", " + n
|
row.ArtistName += ", " + n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if id, err := database.PrimarySongArtistID(db, s.ID); err == nil {
|
||||||
|
row.ArtistID = id
|
||||||
|
}
|
||||||
if album != nil {
|
if album != nil {
|
||||||
row.AlbumTitle = album.Title
|
row.AlbumTitle = album.Title
|
||||||
row.AlbumID = album.ID
|
row.AlbumID = album.ID
|
||||||
|
|||||||
@@ -226,19 +226,33 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.vs-player-title {
|
.vs-player-title {
|
||||||
|
display: block;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vs-player-artist {
|
.vs-player-artist {
|
||||||
|
display: block;
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
color: var(--bs-secondary-color);
|
color: var(--bs-secondary-color);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-player-title:hover,
|
||||||
|
.vs-player-artist:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-player-art[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vs-player-controls {
|
.vs-player-controls {
|
||||||
|
|||||||
@@ -114,7 +114,9 @@
|
|||||||
if (!song) return;
|
if (!song) return;
|
||||||
this.audio.src = "/stream/" + encodeURIComponent(song.id);
|
this.audio.src = "/stream/" + encodeURIComponent(song.id);
|
||||||
this.ui.title.textContent = song.title || "";
|
this.ui.title.textContent = song.title || "";
|
||||||
|
this.ui.title.href = song.albumId ? "/albums/" + encodeURIComponent(song.albumId) : "javascript:void(0)";
|
||||||
this.ui.artist.textContent = song.artist || "";
|
this.ui.artist.textContent = song.artist || "";
|
||||||
|
this.ui.artist.href = song.artistId ? "/artists/" + encodeURIComponent(song.artistId) : "javascript:void(0)";
|
||||||
this.ui.art.src = song.cover ? "/cover-art/" + encodeURIComponent(song.cover) + "?size=100" : "/static/img/default-art.svg";
|
this.ui.art.src = song.cover ? "/cover-art/" + encodeURIComponent(song.cover) + "?size=100" : "/static/img/default-art.svg";
|
||||||
this.ui.starBtn.dataset.itemType = "song";
|
this.ui.starBtn.dataset.itemType = "song";
|
||||||
this.ui.starBtn.dataset.itemId = song.id;
|
this.ui.starBtn.dataset.itemId = song.id;
|
||||||
@@ -307,7 +309,9 @@
|
|||||||
id: node.dataset.songId,
|
id: node.dataset.songId,
|
||||||
title: node.dataset.title,
|
title: node.dataset.title,
|
||||||
artist: node.dataset.artist,
|
artist: node.dataset.artist,
|
||||||
|
artistId: node.dataset.artistId,
|
||||||
album: node.dataset.album,
|
album: node.dataset.album,
|
||||||
|
albumId: node.dataset.albumId,
|
||||||
cover: node.dataset.cover,
|
cover: node.dataset.cover,
|
||||||
starred: node.dataset.starred === "1",
|
starred: node.dataset.starred === "1",
|
||||||
});
|
});
|
||||||
@@ -348,6 +352,20 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var artModalEl = document.getElementById("vs-player-art-modal");
|
||||||
|
if (ui.art && artModalEl && window.bootstrap) {
|
||||||
|
var artModalImg = document.getElementById("vs-player-art-modal-img");
|
||||||
|
var artModal = new bootstrap.Modal(artModalEl);
|
||||||
|
ui.art.addEventListener("click", function () {
|
||||||
|
var song = player.queue[player.index];
|
||||||
|
artModalImg.src = song && song.cover
|
||||||
|
? "/cover-art/" + encodeURIComponent(song.cover) + "?size=1200"
|
||||||
|
: "/static/img/default-art.svg";
|
||||||
|
artModalImg.alt = song ? (song.title || "") : "";
|
||||||
|
artModal.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var lyricsBtn = document.getElementById("vs-lyrics-btn");
|
var lyricsBtn = document.getElementById("vs-lyrics-btn");
|
||||||
if (lyricsBtn && ui.lyricsPanel) {
|
if (lyricsBtn && ui.lyricsPanel) {
|
||||||
lyricsBtn.addEventListener("click", function () {
|
lyricsBtn.addEventListener("click", function () {
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
data-song-id="{{$s.ID}}"
|
data-song-id="{{$s.ID}}"
|
||||||
data-title="{{$s.Title}}"
|
data-title="{{$s.Title}}"
|
||||||
data-artist="{{$s.ArtistName}}"
|
data-artist="{{$s.ArtistName}}"
|
||||||
|
data-artist-id="{{$s.ArtistID}}"
|
||||||
data-album="{{$s.AlbumTitle}}"
|
data-album="{{$s.AlbumTitle}}"
|
||||||
|
data-album-id="{{$s.AlbumID}}"
|
||||||
data-cover="{{$s.CoverArtID}}"
|
data-cover="{{$s.CoverArtID}}"
|
||||||
data-starred="{{if $s.Starred}}1{{else}}0{{end}}">
|
data-starred="{{if $s.Starred}}1{{else}}0{{end}}">
|
||||||
<td class="vs-track-num">{{if $s.TrackNumber}}{{$s.TrackNumber}}{{else}}{{add $i 1}}{{end}}</td>
|
<td class="vs-track-num">{{if $s.TrackNumber}}{{$s.TrackNumber}}{{else}}{{add $i 1}}{{end}}</td>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .User}}{{template "player"}}{{end}}
|
{{if .User}}{{template "player"}}{{template "player_art_modal"}}{{end}}
|
||||||
{{template "shortcuts_modal"}}
|
{{template "shortcuts_modal"}}
|
||||||
{{template "confirm_delete_modal"}}
|
{{template "confirm_delete_modal"}}
|
||||||
|
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
<audio id="vs-audio" preload="metadata"></audio>
|
<audio id="vs-audio" preload="metadata"></audio>
|
||||||
<div id="vs-lyrics-panel" class="vs-lyrics-panel d-none"></div>
|
<div id="vs-lyrics-panel" class="vs-lyrics-panel d-none"></div>
|
||||||
<div class="vs-player">
|
<div class="vs-player">
|
||||||
<img id="vs-player-art" class="vs-player-art" src="/static/img/default-art.svg" alt="">
|
<img id="vs-player-art" class="vs-player-art" src="/static/img/default-art.svg" alt="" role="button" title="View larger photo">
|
||||||
<div class="vs-player-meta">
|
<div class="vs-player-meta">
|
||||||
<div id="vs-player-title" class="vs-player-title">Nothing playing</div>
|
<a id="vs-player-title" class="vs-player-title" href="javascript:void(0)">Nothing playing</a>
|
||||||
<div id="vs-player-artist" class="vs-player-artist"></div>
|
<a id="vs-player-artist" class="vs-player-artist" href="javascript:void(0)"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="vs-player-controls">
|
<div class="vs-player-controls">
|
||||||
<div class="vs-player-buttons">
|
<div class="vs-player-buttons">
|
||||||
@@ -47,6 +47,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "player_art_modal"}}
|
||||||
|
<div class="modal fade" id="vs-player-art-modal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content bg-transparent border-0">
|
||||||
|
<button type="button" class="btn-close btn-close-white ms-auto me-2 mt-2" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
<img id="vs-player-art-modal-img" src="" alt="" class="w-100 rounded">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{define "shortcuts_modal"}}
|
{{define "shortcuts_modal"}}
|
||||||
<div class="modal fade" id="vs-shortcuts-modal" tabindex="-1" aria-hidden="true">
|
<div class="modal fade" id="vs-shortcuts-modal" tabindex="-1" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|||||||
Reference in New Issue
Block a user