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( PlaylistPickerSheet(
song = song, song = song,
loadPlaylists = { repo.getPlaylists() }, loadPlaylists = { repo.getPlaylists() },
coverUrl = { id -> coverUrl(id) },
onAdd = { pl -> onAdd = { pl ->
playlistPickerSong = null playlistPickerSong = null
scope.launch { scope.launch {
@@ -1,9 +1,11 @@
package com.novasonic.app.ui.component package com.novasonic.app.ui.component
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.QueueMusic import androidx.compose.material.icons.automirrored.filled.QueueMusic
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
@@ -11,9 +13,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.novasonic.app.api.model.Playlist import com.novasonic.app.api.model.Playlist
import com.novasonic.app.api.model.Song import com.novasonic.app.api.model.Song
import com.novasonic.app.ui.theme.* import com.novasonic.app.ui.theme.*
@@ -23,6 +28,7 @@ import com.novasonic.app.ui.theme.*
fun PlaylistPickerSheet( fun PlaylistPickerSheet(
song: Song, song: Song,
loadPlaylists: suspend () -> List<Playlist>, loadPlaylists: suspend () -> List<Playlist>,
coverUrl: (String?) -> String?,
onAdd: (Playlist) -> Unit, onAdd: (Playlist) -> Unit,
onCreate: (String) -> Unit, onCreate: (String) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
@@ -117,8 +123,22 @@ fun PlaylistPickerSheet(
.padding(horizontal = 20.dp, vertical = 14.dp), .padding(horizontal = 20.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon(Icons.AutoMirrored.Filled.QueueMusic, null, val cv = coverUrl(pl.coverArt)
tint = NsOnSurface2, modifier = Modifier.size(20.dp)) 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)) Spacer(Modifier.width(14.dp))
Text( Text(
pl.name, pl.name,
+5 -1
View File
@@ -87,7 +87,11 @@ export async function openPlModal(songs) {
} }
S.cachedPlaylists.forEach(pl => { S.cachedPlaylists.forEach(pl => {
const row = document.createElement('div'); row.className = 'pl-row'; 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>`; <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); row.onclick = () => addSongsToPlaylist(pl.id, pl.name);
list.appendChild(row); list.appendChild(row);