The desktop Library's per-record file stats on the phone (library_album_stats, one call for the whole library): FLAC 24/96 or MP3 320k badges on the cards, the size on the list rows and the record page. The artist page's own listening (artist_play_stats): the time played as a chip, the most played track under "On this phone". The whole-library thumbnail warm a moment after boot, at the cards' edge, one decode at a time behind the tiles' gate.
99 lines
5.0 KiB
Svelte
99 lines
5.0 KiB
Svelte
<script>
|
|
/* A record. Stacked and centred, the cover as the page's own hero, the
|
|
track list under it, the artist's other records at the foot. The
|
|
page wears the record's colour (Mobile.svelte reads its art path). */
|
|
import Icon from "../../components/Icon.svelte";
|
|
import Cover from "../Cover.svelte";
|
|
import Release from "../Release.svelte";
|
|
import { lib, albumById, albumsBy, tracksOf } from "../lib.svelte.js";
|
|
import { player, playAlbum, playAlbumShuffled } from "../player.svelte.js";
|
|
import { push, openHalf, openViewer } from "../nav.svelte.js";
|
|
import { mmss, longDur, albumFmt, fmtBytes } from "../util.js";
|
|
|
|
let { id, screenEl = null } = $props();
|
|
|
|
const album = $derived(albumById(id));
|
|
let tracks = $state([]);
|
|
$effect(() => {
|
|
const a = album;
|
|
if (!a) return;
|
|
tracksOf(a.id).then((t) => { tracks = t || []; }, (e) => { player.error = String(e); });
|
|
});
|
|
|
|
const discs = $derived.by(() => {
|
|
const m = new Map();
|
|
for (const t of tracks) {
|
|
const d = t.disc_no ?? 1;
|
|
if (!m.has(d)) m.set(d, []);
|
|
m.get(d).push(t);
|
|
}
|
|
return [...m.entries()].sort((a, b) => a[0] - b[0]);
|
|
});
|
|
const fmt = $derived(albumFmt(tracks));
|
|
const total = $derived(tracks.reduce((n, t) => n + (t.duration_secs || 0), 0));
|
|
const more = $derived(album ? albumsBy(album.artist).filter((x) => x.id !== album.id) : []);
|
|
const various = $derived(tracks.some((t) => t.artist && album && t.artist !== album.artist));
|
|
|
|
function playAt(t) { playAlbum(album, tracks, Math.max(0, tracks.indexOf(t))); }
|
|
function isPlaying(t) { return player.track?.path === t.path; }
|
|
</script>
|
|
|
|
{#if album}
|
|
<div class="rec-head">
|
|
<div class="rec-cover" onclick={() => openViewer(album.art_path)} role="presentation"><Cover path={album.art_path} eager size={48} /></div>
|
|
<div class="rec-meta">
|
|
<div class="rec-title">{album.title}</div>
|
|
<button class="rec-artist" onclick={() => push("artist", { name: album.artist }, screenEl)}>{album.artist}</button>
|
|
<div class="rec-line">{album.year ?? "—"} · {tracks.length || album.track_count} tracks{total ? ` · ${longDur(total)}` : ""}</div>
|
|
{#if fmt}
|
|
<div class="stats rec-chips">
|
|
<span class="s"><Icon name="disc" size={12} strokeWidth={3} />{fmt}</span>
|
|
<span class="s"><Icon name="hard-drive" size={12} strokeWidth={3} />{lib.stats[album.id]?.bytes ? fmtBytes(lib.stats[album.id].bytes) : "in your library"}</span>
|
|
{#if lib.stats[album.id]?.avg_kbps}<span class="s">{lib.stats[album.id].avg_kbps} kbps</span>{/if}
|
|
</div>
|
|
{/if}
|
|
<div class="rec-acts">
|
|
<button class="iconbtn muted" onclick={() => playAlbumShuffled(album, tracks)} aria-label="Shuffle"><Icon name="shuffle" size={22} /></button>
|
|
<button class="disc lg" onclick={() => tracks.length && playAt(tracks[0])} aria-label="Play"><Icon name="play" size={26} /></button>
|
|
<button class="iconbtn muted" onclick={() => openHalf("album", { album, tracks })} aria-label="More"><Icon name="more" size={22} /></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{#each discs as [d, list] (d)}
|
|
{#if discs.length > 1}<div class="disc-head">Disc {d}</div>{/if}
|
|
<div class="tracks">
|
|
{#each list as t, i (t.id)}
|
|
<button class="trk" class:playing={isPlaying(t)} onclick={() => playAt(t)} oncontextmenu={(e) => { e.preventDefault(); openHalf("track", { album, track: t }); }}>
|
|
<span class="n">{t.track_no ?? i + 1}</span>
|
|
<span class="b">
|
|
<span class="t">{t.title}</span>
|
|
{#if various && t.artist}<span class="m">{t.artist}</span>{/if}
|
|
</span>
|
|
<span class="d">{mmss(t.duration_secs)}</span>
|
|
<span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}}
|
|
onclick={(e) => { e.stopPropagation(); openHalf("track", { album, track: t }); }}>
|
|
<Icon name="more" size={17} />
|
|
</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/each}
|
|
|
|
{#if more.length}
|
|
<div class="sec">
|
|
<h2>More by {album.artist}</h2>
|
|
<button class="all" onclick={() => push("artist", { name: album.artist }, screenEl)}>All<Icon name="chevron-right" size={14} /></button>
|
|
</div>
|
|
<div class="rail">
|
|
{#each more as x (x.id)}
|
|
<Release title={x.title} sub={String(x.year || "")} path={x.art_path} onclick={() => push("album", { id: x.id }, screenEl)} />
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{:else}
|
|
<div class="stub" style="margin-top: calc(var(--bar-h) + 10px)"><b>That record is gone.</b>It is no longer in the library.</div>
|
|
{/if}
|
|
|
|
<div class="endpad"></div>
|