Redesign the bar, sheets and cards, and add likes queue, history and sleep timer

Feed modes move into tabs in the bar; likes, history, filters and settings,
and account open as one kind of sheet: a drawer on wide screens, a bottom
sheet with a drag handle on phones, with a scrim, Esc, a focus trap and
focus restore.

Cards get an artist avatar, follow and radio pills, a "new" chip, counts
under round action buttons, an up-next pill and a redrawn waveform with
rounded bars, a reflection and the buffered range. The start overlay names
the track behind a big round play button.

New: undo on dislike, a searchable likes list that plays as a queue
(in order or shuffled), a local history of what played, deep links to a
track (?t=handle/slug), a sleep timer, adjustable crossfade, bass boost,
wheel up (w or a long press on the drop) and more keys.
This commit is contained in:
nvrl committed 2026-11-01 19:33:49 +01:00
1 parent 53ac54b178
commit 62b28bd30e
10 files changed
+1312 -339

No files matched your search

+16 -1
View File
@@ -82,6 +82,12 @@ export class Visualizer {
return this.blob?.lost ? null : this.blob;
}
/// Spin the drop backwards, hard: the rewind.
wheelUp(): void {
this.blob?.spin.fling([0, -26, 0]);
this.start();
}
/// A play/pause from a tap was taken back by the second tap of a double-tap.
cancelFlash(): void {
this.blob?.settleIcon();
@@ -227,7 +233,7 @@ export class Visualizer {
if (!card) return;
const a = this.player.audio;
const progress = a.duration ? a.currentTime / a.duration : 0;
card.drawWave(progress);
card.drawWave(progress, false, a.duration ? bufferedTo(a) / a.duration : 0);
const playing = !a.paused;
const an = this.player.analyser;
const mixed = !!an && this.player.mixing;
@@ -260,3 +266,12 @@ export class Visualizer {
}
};
}
/// How far the audio is loaded from where it plays now, in seconds.
function bufferedTo(a: HTMLMediaElement): number {
const b = a.buffered;
for (let i = 0; i < b.length; i++) {
if (b.start(i) <= a.currentTime + 0.5 && b.end(i) >= a.currentTime) return b.end(i);
}
return a.currentTime;
}