feat(web): My Wave button, mood picker and endless wave playback (M4)
- WaveButton: TypeScript port of the prototype's canvas animation (orb and liquid), breathing at rest, pulsing to the track's BPM and energy in the cover palette, following the cursor, pressing with a spring; reduced motion and hidden tabs slow or stop it - home: the wave in the centre with activity and mood chips; changing them while playing rebuilds the upcoming tracks without stopping - player wave mode: asks for the next batch when two tracks are left, sends complete / skip / early skip / like / dislike as feedback and refreshes the queue right after a reaction or an early skip - tracks expose bpm, energy and loudness once analysed; library page shows analysis progress and 7-day wave metrics; wave style setting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3908814cda
commit
eaedd1fc62
24 files changed
+1193
-244
No files matched your search
@@ -14,6 +14,7 @@ const track = (id: string, duration_ms = 200_000): Track => ({
|
||||
available: true,
|
||||
stream_url: `/s/${id}`,
|
||||
reaction: null,
|
||||
features: null,
|
||||
});
|
||||
|
||||
const ids = (q: PlayQueue) => q.order.map((i) => q.tracks[i]!.id);
|
||||
@@ -62,6 +63,23 @@ describe('PlayQueue', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('PlayQueue in the wave', () => {
|
||||
it('appends batches and drops upcoming tracks', () => {
|
||||
const q = new PlayQueue();
|
||||
q.set([track('a'), track('b'), track('c')]);
|
||||
q.next();
|
||||
expect(q.remaining).toBe(1);
|
||||
q.append([track('d'), track('e')]);
|
||||
expect(q.remaining).toBe(3);
|
||||
q.dropUpcoming();
|
||||
expect(q.current?.id).toBe('b');
|
||||
expect(q.remaining).toBe(0);
|
||||
expect(q.tracks.map((t) => t.id)).toEqual(['a', 'b']);
|
||||
q.append([track('x')]);
|
||||
expect(q.next()?.id).toBe('x');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PlayAttempt', () => {
|
||||
it('counts only time actually played', () => {
|
||||
const a = new PlayAttempt(track('a'));
|
||||
|
||||
@@ -101,6 +101,27 @@ export class PlayQueue {
|
||||
return i < 0 ? null : (this.tracks[this.order[i]!] ?? null);
|
||||
}
|
||||
|
||||
/** Сколько треков осталось после текущего (без учёта повтора). */
|
||||
get remaining(): number {
|
||||
return Math.max(this.order.length - this.pos - 1, 0);
|
||||
}
|
||||
|
||||
/** Добавить треки в конец (бесконечная волна). */
|
||||
append(tracks: Track[]) {
|
||||
const start = this.tracks.length;
|
||||
this.tracks = [...this.tracks, ...tracks];
|
||||
this.order = [...this.order, ...tracks.map((_, i) => start + i)];
|
||||
}
|
||||
|
||||
/** Убрать ещё не начатые треки после текущего (волна пересобирается после реакции). */
|
||||
dropUpcoming() {
|
||||
const keep = this.order.slice(0, this.pos + 1);
|
||||
const tracks = keep.map((i) => this.tracks[i]!);
|
||||
this.tracks = tracks;
|
||||
this.order = tracks.map((_, i) => i);
|
||||
this.pos = Math.max(tracks.length - 1, 0);
|
||||
}
|
||||
|
||||
jump(index: number) {
|
||||
const p = this.order.indexOf(index);
|
||||
if (p >= 0) this.pos = p;
|
||||
|
||||
Reference in new issue
Block a user