feat(web): player bar, playable track lists and Collection (M3)
ci / ts / ts (packages + web) (push) Successful in 53s
release / release (версия и заметки) (push) Successful in 6s
release / android (APK в релиз) (push) Successful in 2m33s

- player bar with cover, like and dislike, previous / play / next,
  shuffle, repeat, seek bar and volume; PlayerProvider lives with the
  signed-in shell and restores the last track and position paused
- clicking a row (or Enter) plays the list from that track with its
  context (album, search, history, collection); the current track is
  highlighted and rows have a like button; a reaction refreshes lists
- Collection: history grouped by day with clearing, likes with "play
  all" and dislikes, all with paging; sidebar section
- events and playback state are flushed when the page is hidden;
  player timers survive StrictMode remounts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 12:11:12 +03:00
1 parent ec3c2810c8
commit 976ffb574b
15 files changed
+803 -49

No files matched your search

+10 -1
View File
@@ -75,6 +75,8 @@ export class Player {
private preloaded: string | null = null;
private refreshedFor: string | null = null;
private timers: ReturnType<typeof setInterval>[] = [];
/** Реакция сохранена на сервере — списки на экране нужно перечитать. */
onReaction: (() => void) | null = null;
constructor(
private readonly session: AuthSession,
@@ -93,6 +95,10 @@ export class Player {
};
this.bindAudio();
this.bindMediaSession();
}
/** Запуск фоновой отправки событий (парный вызов — dispose). */
start() {
this.timers.push(setInterval(() => void this.events.flush(), FLUSH_EVERY_MS));
}
@@ -146,7 +152,8 @@ export class Player {
void this.load(true, this.state.positionMs);
return;
}
void this.audio.play().catch(() => this.update({ playing: false }));
// play() в старых браузерах и jsdom может вернуть undefined вместо промиса.
void Promise.resolve(this.audio.play()).catch(() => this.update({ playing: false }));
}
pause() {
@@ -238,6 +245,7 @@ export class Player {
apply(prev);
throw e;
}
this.onReaction?.();
if (reaction === 'dislike' && this.queue.current?.id === track.id) this.next();
}
@@ -521,6 +529,7 @@ export class Player {
this.audio.pause();
this.audio.removeAttribute('src');
for (const t of this.timers) clearInterval(t);
this.timers = [];
void this.events.flush();
}
}