import { useQueryClient } from '@tanstack/react-query'; import { createContext, useContext, useEffect, useState, useSyncExternalStore, type ReactNode } from 'react'; import { useSession } from '../app/session'; import { Player, type PlayerState } from './player'; const PlayerContext = createContext(null); /** Плеер живёт, пока пользователь вошёл; горячие клавиши и сохранение при уходе со страницы. */ export function PlayerProvider({ children }: { children: ReactNode }) { const session = useSession(); const queryClient = useQueryClient(); const [player] = useState(() => new Player(session)); player.onReaction = () => void queryClient.invalidateQueries(); useEffect(() => { player.start(); void player.restore().catch(() => undefined); const onHide = () => player.onHide(); const onVisibility = () => document.visibilityState === 'hidden' && onHide(); window.addEventListener('pagehide', onHide); document.addEventListener('visibilitychange', onVisibility); const onKey = (e: KeyboardEvent) => handleHotkey(player, e); document.addEventListener('keydown', onKey); return () => { window.removeEventListener('pagehide', onHide); document.removeEventListener('visibilitychange', onVisibility); document.removeEventListener('keydown', onKey); player.dispose(); }; }, [player]); return {children}; } /** Пробел — пауза, ←/→ — ±10 с, Shift+←/→ — трек, L — лайк (docs/web.md). */ export function handleHotkey(player: Player, e: KeyboardEvent) { const target = e.target as HTMLElement | null; if (target?.closest('input, textarea, select, [contenteditable="true"], [role="dialog"]')) return; if (e.ctrlKey || e.metaKey || e.altKey) return; const s = player.getState(); if (!s.track) return; switch (e.key) { case ' ': if (target?.closest('button, a')) return; // пробел на кнопке нажимает её e.preventDefault(); player.toggle(); break; case 'ArrowLeft': e.preventDefault(); if (e.shiftKey) player.prev(); else player.seek(s.positionMs - 10_000); break; case 'ArrowRight': e.preventDefault(); if (e.shiftKey) player.next(); else player.seek(s.positionMs + 10_000); break; case 'l': case 'L': case 'д': case 'Д': e.preventDefault(); void player.react(s.track, s.track.reaction === 'like' ? null : 'like').catch(() => undefined); break; } } export function usePlayer(): Player { const p = useContext(PlayerContext); if (!p) throw new Error('usePlayer outside PlayerProvider'); return p; } export function usePlayerState(): PlayerState { const p = usePlayer(); return useSyncExternalStore(p.subscribe, p.getState); }