feat(playlists): show playlist cover in the "add to playlist" picker (web + Android)

The picker listed playlists with a generic list icon; now each row shows the
playlist's cover art (falling back to the icon when there's none), so you can
tell them apart at a glance when adding a song.

- web (modals.js): 34px cover thumbnail per row.
- android (PlaylistPickerSheet): 40dp AsyncImage cover, coverUrl passed from AppNavigation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-07-05 23:21:18 +02:00
1 parent 1775ec5b9b
commit 17a28b3011
3 files changed
+28 -3

No files matched your search

@@ -464,6 +464,7 @@ fun AppNavigation() {
PlaylistPickerSheet(
song = song,
loadPlaylists = { repo.getPlaylists() },
coverUrl = { id -> coverUrl(id) },
onAdd = { pl ->
playlistPickerSong = null
scope.launch {
@@ -1,9 +1,11 @@
package com.novasonic.app.ui.component
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.QueueMusic
import androidx.compose.material.icons.filled.Add
@@ -11,9 +13,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.novasonic.app.api.model.Playlist
import com.novasonic.app.api.model.Song
import com.novasonic.app.ui.theme.*
@@ -23,6 +28,7 @@ import com.novasonic.app.ui.theme.*
fun PlaylistPickerSheet(
song: Song,
loadPlaylists: suspend () -> List<Playlist>,
coverUrl: (String?) -> String?,
onAdd: (Playlist) -> Unit,
onCreate: (String) -> Unit,
onDismiss: () -> Unit,
@@ -117,8 +123,22 @@ fun PlaylistPickerSheet(
.padding(horizontal = 20.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(Icons.AutoMirrored.Filled.QueueMusic, null,
tint = NsOnSurface2, modifier = Modifier.size(20.dp))
val cv = coverUrl(pl.coverArt)
Box(
Modifier.size(40.dp).clip(RoundedCornerShape(6.dp)).background(NsSurface2),
contentAlignment = Alignment.Center,
) {
if (cv != null) {
AsyncImage(
model = cv, contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
} else {
Icon(Icons.AutoMirrored.Filled.QueueMusic, null,
tint = NsOnSurface2, modifier = Modifier.size(20.dp))
}
}
Spacer(Modifier.width(14.dp))
Text(
pl.name,
+5 -1
View File
@@ -87,7 +87,11 @@ export async function openPlModal(songs) {
}
S.cachedPlaylists.forEach(pl => {
const row = document.createElement('div'); row.className = 'pl-row';
row.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/></svg>
const cv = pl.coverArt ? coverUrl(pl.coverArt, 96) : null;
const inner = cv
? `<img src="${cv}" alt="" style="width:100%;height:100%;object-fit:cover" onerror="this.remove()">`
: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/></svg>';
row.innerHTML = `<span style="width:34px;height:34px;border-radius:6px;flex-shrink:0;background:var(--surface2);display:flex;align-items:center;justify-content:center;overflow:hidden;color:var(--text3)">${inner}</span>
<span>${esc(pl.name)}</span><span style="margin-left:auto;font-size:11px;color:var(--text3)">${pl.songCount} Titel</span>`;
row.onclick = () => addSongsToPlaylist(pl.id, pl.name);
list.appendChild(row);