Files
SoulStream/src/mobile/screens/Album.svelte
T
KeelrLeek 991d52d0cc android: the library's file stats on the cards, the artist's own plays, the thumbnail prewarm
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.
2026-09-13 20:37:51 -05:00

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>