import {Registry} from "tc-shared/events"; import {MusicPlaylistUiEvents} from "tc-shared/ui/frames/side/MusicPlaylistDefinitions"; import {DefaultThumbnail, formatPlaytime, MusicPlaylistList} from "tc-shared/ui/frames/side/MusicPlaylistRenderer"; import * as React from "react"; import {useContext, useEffect, useRef, useState} from "react"; import { MusicBotPlayerState, MusicBotPlayerTimestamp, MusicBotSongInfo, MusicBotUiEvents } from "tc-shared/ui/frames/side/MusicBotDefinitions"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {showImagePreview} from "tc-shared/ui/frames/ImagePreview"; import {Slider} from "tc-shared/ui/react-elements/Slider"; const cssStyle = require("./MusicBotRenderer.scss"); const EventContext = React.createContext>(undefined); const SongInfoContext = React.createContext(undefined); const TimestampContext = React.createContext(undefined); const ButtonRewind = () => ( ); const ButtonPlay = () => ( ); const ButtonPause = () => ( ); const ButtonForward = () => ( ); const ButtonVolume = () => ( ); const SongInfoProvider = (props) => { const events = useContext(EventContext); const [ info, setInfo ] = useState(() => { events.fire("query_song_info"); return { type: "none" }; }); events.reactUse("notify_song_info", event => setInfo(event.info)); return ( {props.children} ); }; const PlayerTimestampProvider = (props) => { const events = useContext(EventContext); const [ timestamp, setTimestamp ] = useState(() => { events.fire("query_player_timestamp"); return { seekable: false, bufferOffset: 0, playOffset: 0, base: 0, total: 0, }; }); const [ seekOffset, setSeekOffset ] = useState(undefined); events.reactUse("notify_player_timestamp", event => setTimestamp(event.timestamp), undefined, []); events.reactUse("notify_player_seek_timestamp", event => { if(event.applySeek && timestamp.base > 0 && typeof seekOffset === "number") { timestamp.playOffset = seekOffset; timestamp.bufferOffset += Date.now() - timestamp.base; timestamp.base = Date.now(); } setSeekOffset(event.offset); }, undefined, [ seekOffset, timestamp ]); return ( {props.children} ) } const Thumbnail = React.memo(() => { const info = useContext(SongInfoContext); let thumbnail; switch (info.type) { case "none": thumbnail = ; break; case "loading": thumbnail = ; break; case "song": if(info.thumbnail) { thumbnail = ( showImagePreview(info.thumbnail, info.thumbnail)} alt={tr("Thumbnail")} style={{ cursor: "pointer" }} /> ); } else { thumbnail = ; } break; } return (
{thumbnail}
); }); const Timestamps = () => { const info = useContext(TimestampContext); const [ revision, setRevision ] = useState(0); useEffect(() => { const id = setTimeout(() => setRevision(revision + 1), 990); return () => clearTimeout(id); }); let current: number; if(info.seekable && typeof info.seekOffset === "number") { current = info.seekOffset; } else { const timePassed = info.base > 0 ? Date.now() - info.base : 0; current = info.playOffset + timePassed; } return (
{formatPlaytime(current)}
{formatPlaytime(info.total)}
); } const Timeline = () => { const events = useContext(EventContext); const info = useContext(TimestampContext); const refContainer = useRef(); const [ moveActive, setMoveActive ] = useState(false); useEffect(() => { if(!moveActive) { return; } document.body.classList.add(cssStyle.bodySeek); let currentSeekOffset; const mouseMoveListener = (event: MouseEvent) => { if(!refContainer.current) { return; } const { x, width } = refContainer.current.getBoundingClientRect(); if(event.pageX <= x) { events.fire("notify_player_seek_timestamp", { offset: currentSeekOffset = 0, applySeek: false }); } else if(event.pageX >= x + width) { events.fire("notify_player_seek_timestamp", { offset: currentSeekOffset = info.total, applySeek: false }); } else { events.fire("notify_player_seek_timestamp", { offset: currentSeekOffset = Math.floor((event.pageX - x) / width * info.total), applySeek: false }); } }; const mouseUpListener = () => { if(typeof currentSeekOffset === "number") { events.fire("action_seek_to", { target: currentSeekOffset }); } events.fire("notify_player_seek_timestamp", { offset: undefined, applySeek: true }); setMoveActive(false); } document.addEventListener("mousemove", mouseMoveListener); document.addEventListener("mouseleave", mouseUpListener); document.addEventListener("mouseup", mouseUpListener); document.addEventListener("focusout", mouseUpListener); return () => { document.body.classList.remove(cssStyle.bodySeek); document.removeEventListener("mousemove", mouseMoveListener); document.removeEventListener("mouseleave", mouseUpListener); document.removeEventListener("mouseup", mouseUpListener); document.removeEventListener("focusout", mouseUpListener); }; }, [ moveActive ]); let current: number, buffered: number; const timePassed = info.base > 0 ? Date.now() - info.base : 0; if(info.seekable && typeof info.seekOffset === "number") { current = info.seekOffset; } else { current = info.playOffset + timePassed; } buffered = info.bufferOffset + timePassed; let widthBuffered = info.total === 0 ? 100 : (buffered / info.total) * 100; let widthCurrent = info.total === 0 ? 100 : (current / info.total) * 100; return (
info.seekable && info.total > 0 && setMoveActive(true)} >
); } const SongInfo = () => { const info = useContext(SongInfoContext); let name, nameTitle/*, description */; switch (info.type) { case "none": name = No song selected; break; case "song": name = info.title || info.url; nameTitle = name; /* description = info.description; */ break; case "loading": name = info.url; nameTitle = name; break; } return ( ); } const ControlButtons = () => { const events = useContext(EventContext); const [ playerState, setPlayerState ] = useState(() => { events.fire("query_player_state"); return "paused"; }); events.reactUse("notify_player_state", event => setPlayerState(event.state)); let playButton; if(playerState === "paused") { playButton = (
events.fire("action_player_action", { action: "play" })}>
); } else { playButton = (
events.fire("action_player_action", { action: "pause" })}>
); } return (
events.fire("action_player_action", { action: "rewind" })}>
{playButton}
events.fire("action_player_action", { action: "forward" })}>
); } const VolumeSlider = (props: { mode: "local" | "remote", }) => { const events = useContext(EventContext); const refSlider = useRef(); const [ value, setValue ] = useState(() => { events.fire("query_volume", { mode: props.mode }); return 100; }); events.reactUse("notify_volume", event => { if(event.mode !== props.mode) { return; } setValue(event.volume * 100); if(!refSlider.current?.state.active) { refSlider.current?.setState({ value: event.volume * 100 }); } }); let name; if(props.mode === "local") { name = Local; } else { name = Remote; } const valueString = (value: number) => { if(value > 100) { return "+" + (value - 100).toFixed(0); } else if(value == 100) { return "±0"; } else { return "-" + (100 - value).toFixed(0); } } return (
{ setValue(value); if(props.mode === "local") { events.fire("action_change_volume", { mode: props.mode, volume: value / 100 }); } }} onChange={value => { setValue(value); events.fire("action_change_volume", { mode: props.mode, volume: value / 100 }); }} tooltip={value => valueString(value) + "%"} />
); } const VolumeSetting = () => { const events = useContext(EventContext); const [ expended, setExpended ] = useState(false); events.reactUse("notify_bot_changed", () => setExpended(false)); return (
setExpended(!expended)}>
); } const MusicBotPlayer = () => { return (
); } export const MusicBotRenderer = (props: { botEvents: Registry, playlistEvents: Registry }) => { return (
); }