import { useCallback, useEffect, useRef, useState } from "react"; import { api } from "./api"; import { Login } from "./components/Login"; import { NowPlaying } from "./components/NowPlaying"; import { QueueList } from "./components/QueueList"; import { SearchPanel } from "./components/SearchPanel"; import type { Me, PlayerState, ServerStateResponse, VoiceChannel } from "./types"; const SERVER_KEY = "mbot.server"; export function App() { const [me, setMe] = useState(null); const [loading, setLoading] = useState(true); const [serverId, setServerId] = useState(null); const [state, setState] = useState(null); const [position, setPosition] = useState(0); const [voiceChannels, setVoiceChannels] = useState([]); const [yourVoiceChannel, setYourVoiceChannel] = useState(null); const [canControl, setCanControl] = useState(false); const [presenceKnown, setPresenceKnown] = useState(true); const [error, setError] = useState(null); const socketRef = useRef(null); /** Consumes the one-time link issued by the `!panel` chat command. */ const consumeLinkToken = useCallback(async (): Promise => { const params = new URLSearchParams(window.location.search); const token = params.get("token"); if (!token) return null; try { const result = await api.loginWithLink(token); window.history.replaceState({}, "", "/"); return result.serverId; } catch (err) { setError(err instanceof Error ? err.message : "Ссылка недействительна"); window.history.replaceState({}, "", "/"); return null; } }, []); const loadMe = useCallback( async (preferredServer?: string | null) => { try { const profile = await api.me(); setMe(profile); const stored = preferredServer ?? localStorage.getItem(SERVER_KEY); const chosen = profile.servers.find((server) => server.id === stored) ?? profile.servers[0]; setServerId(chosen?.id ?? null); } catch { setMe(null); } finally { setLoading(false); } }, [], ); useEffect(() => { void (async () => { const fromLink = await consumeLinkToken(); await loadMe(fromLink); })(); }, [consumeLinkToken, loadMe]); const applyServerState = useCallback((payload: ServerStateResponse) => { setState(payload.state); setPosition(payload.state.position); setVoiceChannels(payload.voiceChannels); setYourVoiceChannel(payload.yourVoiceChannel); setPresenceKnown(payload.voicePresenceKnown); setCanControl(payload.canControl); }, []); const refresh = useCallback( async (id: string) => { try { applyServerState(await api.state(id)); } catch (err) { setError(err instanceof Error ? err.message : "Не удалось получить состояние"); } }, [applyServerState], ); // Live updates: the socket carries both full snapshots and 1 Hz position ticks. useEffect(() => { if (!serverId) return; localStorage.setItem(SERVER_KEY, serverId); void refresh(serverId); const protocol = window.location.protocol === "https:" ? "wss" : "ws"; const socket = new WebSocket(`${protocol}://${window.location.host}/ws?server=${serverId}`); socketRef.current = socket; socket.onmessage = (event) => { const payload = JSON.parse(event.data as string) as | { type: "state"; state: PlayerState } | { type: "position"; position: number } | { type: "presence"; yourVoiceChannel: VoiceChannel | null; voiceChannels: VoiceChannel[]; voicePresenceKnown: boolean; }; if (payload.type === "state") { setState(payload.state); setPosition(payload.state.position); } else if (payload.type === "presence") { setYourVoiceChannel(payload.yourVoiceChannel); setVoiceChannels(payload.voiceChannels); setPresenceKnown(payload.voicePresenceKnown); } else { setPosition(payload.position); } }; return () => { socket.close(); socketRef.current = null; }; }, [serverId, refresh]); const runAction = useCallback( async (action: string, payload: Record = {}) => { if (!serverId) return; setError(null); try { await api.action(serverId, action, payload); } catch (err) { setError(err instanceof Error ? err.message : "Действие не выполнено"); } }, [serverId], ); if (loading) return
Загрузка…
; if (!me) return void loadMe()} />; return (
Stoat Music
{me.servers.length > 0 && ( )}
{me.user.username}
{error &&
{error}
} {!serverId || !state ? (
Бот не состоит ни в одном общем с вами сервере. Пригласите его и обновите страницу.
) : (
void runAction(action, payload)} onSeek={(seconds) => void runAction("seek", { position: seconds })} onJoin={(channelId) => { void api .join(serverId, channelId) .then(() => refresh(serverId)) .catch((err: Error) => setError(err.message)); }} /> { void api.removeTrack(serverId, track.id).catch((err: Error) => setError(err.message)); }} onMove={(track, index) => { void api.moveTrack(serverId, track.id, index).catch((err: Error) => setError(err.message)); }} onClear={() => void runAction("clear")} /> {state.history.length > 0 && (

История

    {state.history.map((track, index) => (
  • {index + 1}
    {track.title}
    {track.author ?? ""}
  • ))}
)}
)}
); }