Music: close button no longer offsets the Now Playing art, Family playlists get a 2x2 art cover, phone tabs fit one row with a More menu
build-container / build (push) Successful in 3m9s
build-container / build (push) Successful in 3m9s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e248bac143
commit
94ad5a8e1d
3 files changed
+29
-8
No files matched your search
+11
-4
@@ -50,9 +50,9 @@ async function mmRandomTracks(sec, n, pls = []) {
|
||||
}
|
||||
|
||||
// ---- a page of songs (a mix, a genre, favorites...): Play, Shuffle, Save as playlist, and the list
|
||||
function mmPage(v, { title, sub, thumb, tracks, back, about, extra = '', onChange }) {
|
||||
function mmPage(v, { title, sub, thumb, cover, tracks, back, about, extra = '', onChange }) {
|
||||
const hrefBack = back ? `<div class="bar"><a class="btn ghost" href="${back[0]}">${icon('back')}${esc(back[1])}</a></div>` : '';
|
||||
v.innerHTML = `<div class="wrap">${hrefBack}<div class="mhead">${thumb ? `<img class="art" alt="" src="${px.art(thumb, 400)}">` : `<div class="art mixart" style="background:linear-gradient(135deg,hsl(${mmHue(title)} 55% 42%),hsl(${mmHue(title) + 40} 55% 26%))"></div>`}<div><h1>${esc(title)}</h1><p class="sub">${esc(sub || tracks.length + ' songs')}</p></div></div>
|
||||
v.innerHTML = `<div class="wrap">${hrefBack}<div class="mhead">${cover ? cover : thumb ? `<img class="art" alt="" src="${px.art(thumb, 400)}">` : `<div class="art mixart" style="background:linear-gradient(135deg,hsl(${mmHue(title)} 55% 42%),hsl(${mmHue(title) + 40} 55% 26%))"></div>`}<div><h1>${esc(title)}</h1><p class="sub">${esc(sub || tracks.length + ' songs')}</p></div></div>
|
||||
${about ? `<details class="mmabout"><summary>About</summary><p>${esc(about).replace(/\n/g, '<br>')}</p></details>` : ''}
|
||||
<div class="bar"><button class="btn" id="mpa">${icon('play')}Play</button><button class="btn ghost" id="mps">${icon('shuffle')}Shuffle</button><button class="btn ghost" id="mpl">${icon('plus')}Save as playlist</button>${extra}</div>
|
||||
<div class="list">${tracks.map((t, i) => `<div class="row it" data-i="${i}"><button class="rmain" data-p="${i}"><span class="num">${i + 1}</span><span class="grow"><b>${esc(t.title)}</b><small>${esc(t.artist)}</small></span><small>${fmtDur(t.dur)}</small></button>
|
||||
@@ -229,13 +229,20 @@ async function musicStats({ v, st }, range = 'month') {
|
||||
}
|
||||
|
||||
// ================================================================ Family playlists
|
||||
const famCard = p => `<a class="acard" href="#/music/family/${p.id}"><span class="mixart" style="background:linear-gradient(135deg,hsl(${mmHue(p.name)} 55% 42%),hsl(${mmHue(p.name) + 40} 55% 26%))"><b>${esc(p.name)}</b></span><small>${p.count} songs · ${esc(p.ownerName)}</small></a>`;
|
||||
/** Plex-style cover for a playlist: the art of its songs in a 2x2 grid (one picture if they share one), or a colored tile when no song has art. */
|
||||
const mmMosaic = (thumbs, name, cls = '') => {
|
||||
const u = [...new Set((thumbs || []).filter(Boolean))].slice(0, 4);
|
||||
if (!u.length) return `<span class="mixart ${cls}" style="background:linear-gradient(135deg,hsl(${mmHue(name)} 55% 42%),hsl(${mmHue(name) + 40} 55% 26%))"></span>`;
|
||||
const img = t => `<img loading="lazy" alt="" src="${px.art(t, 300)}">`;
|
||||
return `<span class="mosaic ${cls}${u.length < 4 ? ' one' : ''}">${(u.length < 4 ? u.slice(0, 1) : u).map(img).join('')}</span>`;
|
||||
};
|
||||
const famCard = p => `<a class="acard" href="#/music/family/${p.id}">${mmMosaic(p.tracks.slice(-40).reverse().map(t => t.thumb), p.name)}<b>${esc(p.name)}</b><small>${p.count} songs · ${esc(p.ownerName)}</small></a>`;
|
||||
async function musicFamilyPage({ v }, id) {
|
||||
mmLoading(v, 'Family playlist');
|
||||
let p; try { p = await mmApi('/family/' + id); } catch (e) { v.innerHTML = `<div class="wrap"><div class="bar"><a class="btn ghost" href="#/music/playlists">${icon('back')}Playlists</a></div><p class="err">${esc(e.message)}</p></div>`; return; }
|
||||
const tracks = await mmTracksByKeys(p.tracks.map(t => t.key)); if (!location.hash.startsWith('#/music/family')) return;
|
||||
const mineOrAdmin = ME && (ME.role === 'admin' || ME.id === p.owner);
|
||||
mmPage(v, { title: p.name, sub: `${tracks.length} songs · a Family playlist by ${p.ownerName}. Everyone can add and remove songs.`, tracks, back: ['#/music/playlists', 'Playlists'], extra: `${mineOrAdmin ? '<button class="btn ghost" id="mfr">Rename</button><button class="btn danger" id="mfd">Delete</button>' : ''}`, onChange: () => {} });
|
||||
mmPage(v, { title: p.name, cover: mmMosaic(tracks.map(t => t.thumb), p.name, 'art'), sub: `${tracks.length} songs · a Family playlist by ${p.ownerName}. Everyone can add and remove songs.`, tracks, back: ['#/music/playlists', 'Playlists'], extra: `${mineOrAdmin ? '<button class="btn ghost" id="mfr">Rename</button><button class="btn danger" id="mfd">Delete</button>' : ''}`, onChange: () => {} });
|
||||
v.querySelectorAll('.row.it').forEach(r => { const t = tracks[+r.dataset.i], b = document.createElement('button'); b.className = 'ibtn'; b.setAttribute('aria-label', 'Remove from this playlist'); b.innerHTML = icon('trash'); b.onclick = async () => { try { await mmApi(`/family/${id}/tracks/${t.key}`, { method: 'DELETE' }); toast('Removed'); route(); } catch (e) { toast(e.message); } }; r.appendChild(b); });
|
||||
if ($('#mfr')) $('#mfr').onclick = () => textSheet('Rename playlist', 'Playlist name', 'Rename', async n => { try { await mmApi('/family/' + id, mmOpt('POST', { name: n })); route(); } catch (e) { toast(e.message); } }, p.name);
|
||||
if ($('#mfd')) $('#mfd').onclick = () => confirmSheet('Delete this Family playlist?', 'It disappears for everyone.', 'Delete', async () => { try { await mmApi('/family/' + id, { method: 'DELETE' }); toast('Deleted'); location.hash = '#/music/playlists'; } catch (e) { toast(e.message); } });
|
||||
|
||||
Reference in new issue
Block a user