mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 18:44:16 +00:00
Support for live radios
This commit is contained in:
parent
5f3fb3a013
commit
6eef3480b1
@ -14,29 +14,35 @@ import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
|||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const EventsHandlers = {
|
const EventsHandlers = {
|
||||||
"playback": () => {
|
playback: (state) => {
|
||||||
|
if (state.live) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return app.cores.player.playback.toggle()
|
return app.cores.player.playback.toggle()
|
||||||
},
|
},
|
||||||
"previous": () => {
|
previous: () => {
|
||||||
return app.cores.player.playback.previous()
|
return app.cores.player.playback.previous()
|
||||||
},
|
},
|
||||||
"next": () => {
|
next: () => {
|
||||||
return app.cores.player.playback.next()
|
return app.cores.player.playback.next()
|
||||||
},
|
},
|
||||||
"volume": (ctx, value) => {
|
volume: (ctx, value) => {
|
||||||
return app.cores.player.controls.volume(value)
|
return app.cores.player.controls.volume(value)
|
||||||
},
|
},
|
||||||
"mute": () => {
|
mute: () => {
|
||||||
return app.cores.player.controls.mute("toggle")
|
return app.cores.player.controls.mute("toggle")
|
||||||
},
|
},
|
||||||
"like": async (ctx) => {
|
like: async (ctx) => {
|
||||||
if (!ctx.track_manifest) {
|
if (!ctx.track_manifest) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const track = app.cores.player.track()
|
const track = app.cores.player.track()
|
||||||
|
|
||||||
return await track.manifest.serviceOperations.toggleItemFavourite("track", ctx.track_manifest._id)
|
return await track.manifest.serviceOperations.toggleItemFavourite(
|
||||||
|
"track",
|
||||||
|
ctx.track_manifest._id,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,76 +57,79 @@ const Controls = (props) => {
|
|||||||
return EventsHandlers[event](playerState, ...args)
|
return EventsHandlers[event](playerState, ...args)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div
|
return (
|
||||||
className={
|
<div className={props.className ?? "player-controls"}>
|
||||||
props.className ?? "player-controls"
|
<AudioPlayerChangeModeButton disabled={props.streamMode} />
|
||||||
}
|
|
||||||
>
|
|
||||||
<AudioPlayerChangeModeButton
|
|
||||||
disabled={playerState.control_locked}
|
|
||||||
/>
|
|
||||||
<antd.Button
|
<antd.Button
|
||||||
type="ghost"
|
type="ghost"
|
||||||
shape="round"
|
shape="round"
|
||||||
icon={<Icons.FiChevronLeft />}
|
icon={<Icons.FiChevronLeft />}
|
||||||
onClick={() => handleAction("previous")}
|
onClick={() => handleAction("previous")}
|
||||||
disabled={playerState.control_locked}
|
disabled={props.streamMode}
|
||||||
/>
|
/>
|
||||||
<antd.Button
|
<antd.Button
|
||||||
type="primary"
|
type="primary"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
icon={playerState.livestream_mode ? <Icons.MdStop /> : playerState.playback_status === "playing" ? <Icons.MdPause /> : <Icons.MdPlayArrow />}
|
icon={
|
||||||
|
props.streamMode ? (
|
||||||
|
<Icons.MdStop />
|
||||||
|
) : playerState.playback_status === "playing" ? (
|
||||||
|
<Icons.MdPause />
|
||||||
|
) : (
|
||||||
|
<Icons.MdPlayArrow />
|
||||||
|
)
|
||||||
|
}
|
||||||
onClick={() => handleAction("playback")}
|
onClick={() => handleAction("playback")}
|
||||||
className="playButton"
|
className="playButton"
|
||||||
disabled={playerState.control_locked}
|
|
||||||
>
|
>
|
||||||
{
|
{playerState.loading && (
|
||||||
playerState.loading && <div className="loadCircle">
|
<div className="loadCircle">
|
||||||
<UseAnimations
|
<UseAnimations
|
||||||
animation={LoadingAnimation}
|
animation={LoadingAnimation}
|
||||||
size="100%"
|
size="100%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</antd.Button>
|
</antd.Button>
|
||||||
<antd.Button
|
<antd.Button
|
||||||
type="ghost"
|
type="ghost"
|
||||||
shape="round"
|
shape="round"
|
||||||
icon={<Icons.FiChevronRight />}
|
icon={<Icons.FiChevronRight />}
|
||||||
onClick={() => handleAction("next")}
|
onClick={() => handleAction("next")}
|
||||||
disabled={playerState.control_locked}
|
disabled={props.streamMode}
|
||||||
/>
|
/>
|
||||||
{
|
{!app.isMobile && (
|
||||||
!app.isMobile && <antd.Popover
|
<antd.Popover
|
||||||
content={React.createElement(
|
content={React.createElement(AudioVolume, {
|
||||||
AudioVolume,
|
|
||||||
{
|
|
||||||
onChange: (value) => handleAction("volume", value),
|
onChange: (value) => handleAction("volume", value),
|
||||||
defaultValue: playerState.volume
|
defaultValue: playerState.volume,
|
||||||
}
|
})}
|
||||||
)}
|
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="muteButton"
|
className="muteButton"
|
||||||
onClick={() => handleAction("mute")}
|
onClick={() => handleAction("mute")}
|
||||||
>
|
>
|
||||||
{
|
{playerState.muted ? (
|
||||||
playerState.muted
|
<Icons.FiVolumeX />
|
||||||
? <Icons.FiVolumeX />
|
) : (
|
||||||
: <Icons.FiVolume2 />
|
<Icons.FiVolume2 />
|
||||||
}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</antd.Popover>
|
</antd.Popover>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{
|
{app.isMobile && (
|
||||||
app.isMobile && <LikeButton
|
<LikeButton
|
||||||
liked={playerState.track_manifest?.serviceOperations.fetchLikeStatus}
|
liked={
|
||||||
|
playerState.track_manifest?.serviceOperations
|
||||||
|
.fetchLikeStatus
|
||||||
|
}
|
||||||
onClick={() => handleAction("like")}
|
onClick={() => handleAction("like")}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Controls
|
export default Controls
|
@ -17,30 +17,36 @@ const ExtraActions = (props) => {
|
|||||||
|
|
||||||
const track = app.cores.player.track()
|
const track = app.cores.player.track()
|
||||||
|
|
||||||
await track.manifest.serviceOperations.toggleItemFavourite("track", playerState.track_manifest._id)
|
await track.manifest.serviceOperations.toggleItemFavourite(
|
||||||
|
"track",
|
||||||
|
playerState.track_manifest._id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="extra_actions">
|
return (
|
||||||
{
|
<div className="extra_actions">
|
||||||
app.isMobile && <Button
|
{app.isMobile && (
|
||||||
|
<Button
|
||||||
type="ghost"
|
type="ghost"
|
||||||
icon={<Icons.MdAbc />}
|
icon={<Icons.MdAbc />}
|
||||||
disabled={!playerState.track_manifest?.lyrics_enabled}
|
disabled={!playerState.track_manifest?.lyrics_enabled}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{
|
{!app.isMobile && (
|
||||||
!app.isMobile && playerState.track_manifest?._id && <LikeButton
|
<LikeButton
|
||||||
liked={playerState.track_manifest?.serviceOperations.fetchLikeStatus}
|
liked={
|
||||||
|
playerState.track_manifest?.serviceOperations
|
||||||
|
.fetchLikeStatus
|
||||||
|
}
|
||||||
onClick={handleClickLike}
|
onClick={handleClickLike}
|
||||||
|
disabled={!playerState.track_manifest?._id}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button type="ghost" icon={<Icons.MdQueueMusic />} />
|
||||||
type="ghost"
|
|
||||||
icon={<Icons.MdQueueMusic />}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExtraActions
|
export default ExtraActions
|
51
packages/app/src/components/Player/LiveInfo/index.jsx
Normal file
51
packages/app/src/components/Player/LiveInfo/index.jsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React from "react"
|
||||||
|
import SSEEvents from "@classes/SSEEvents"
|
||||||
|
|
||||||
|
import { MdPlayCircle, MdHeadphones } from "react-icons/md"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const LiveInfo = ({ radioId, initialData }) => {
|
||||||
|
const [data, setData] = React.useState(initialData ?? {})
|
||||||
|
|
||||||
|
const eventManager = React.useMemo(
|
||||||
|
() =>
|
||||||
|
new SSEEvents(
|
||||||
|
`${app.cores.api.client().mainOrigin}/music/radio/sse/radio:${radioId}`,
|
||||||
|
{
|
||||||
|
update: (data) => {
|
||||||
|
if (typeof data.now_playing === "string") {
|
||||||
|
data.now_playing = JSON.parse(data.now_playing)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Radio data updated`, data)
|
||||||
|
setData(data)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
eventManager.close()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="live-info">
|
||||||
|
{data.now_playing && (
|
||||||
|
<>
|
||||||
|
<div className="live-info-title">
|
||||||
|
<MdPlayCircle /> {data.now_playing.song.text}
|
||||||
|
</div>
|
||||||
|
<div className="live-info-listeners">
|
||||||
|
<MdHeadphones /> {data.listeners}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LiveInfo
|
8
packages/app/src/components/Player/LiveInfo/index.less
Normal file
8
packages/app/src/components/Player/LiveInfo/index.less
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.live-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
}
|
@ -32,7 +32,8 @@ export default class SeekBar extends React.Component {
|
|||||||
|
|
||||||
calculateDuration = (preCalculatedDuration) => {
|
calculateDuration = (preCalculatedDuration) => {
|
||||||
// get current audio duration
|
// get current audio duration
|
||||||
const audioDuration = preCalculatedDuration ?? app.cores.player.controls.duration()
|
const audioDuration =
|
||||||
|
preCalculatedDuration ?? app.cores.player.controls.duration()
|
||||||
|
|
||||||
if (isNaN(audioDuration)) {
|
if (isNaN(audioDuration)) {
|
||||||
return
|
return
|
||||||
@ -40,7 +41,7 @@ export default class SeekBar extends React.Component {
|
|||||||
|
|
||||||
// set duration
|
// set duration
|
||||||
this.setState({
|
this.setState({
|
||||||
durationText: seekToTimeLabel(audioDuration)
|
durationText: seekToTimeLabel(audioDuration),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +51,7 @@ export default class SeekBar extends React.Component {
|
|||||||
|
|
||||||
// set time
|
// set time
|
||||||
this.setState({
|
this.setState({
|
||||||
timeText: seekToTimeLabel(seek)
|
timeText: seekToTimeLabel(seek),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ export default class SeekBar extends React.Component {
|
|||||||
const percent = (seek / duration) * 100
|
const percent = (seek / duration) * 100
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
sliderTime: percent
|
sliderTime: percent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +113,9 @@ export default class SeekBar extends React.Component {
|
|||||||
sliderTime: 0,
|
sliderTime: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.calculateDuration(manifest.metadata?.duration ?? manifest.duration)
|
this.calculateDuration(
|
||||||
|
manifest.metadata?.duration ?? manifest.duration,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
"player.seeked": (seek) => {
|
"player.seeked": (seek) => {
|
||||||
this.calculateTime()
|
this.calculateTime()
|
||||||
@ -158,38 +161,37 @@ export default class SeekBar extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div
|
return (
|
||||||
className={classnames(
|
<div
|
||||||
"player-seek_bar",
|
className={classnames("player-seek_bar", {
|
||||||
{
|
|
||||||
["stopped"]: this.props.stopped,
|
["stopped"]: this.props.stopped,
|
||||||
}
|
})}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames("progress", {
|
||||||
"progress",
|
|
||||||
{
|
|
||||||
["hidden"]: this.props.streamMode,
|
["hidden"]: this.props.streamMode,
|
||||||
}
|
})}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Slider
|
<Slider
|
||||||
size="small"
|
size="small"
|
||||||
value={this.state.sliderTime}
|
value={this.state.sliderTime}
|
||||||
disabled={this.props.stopped || this.props.streamMode || this.props.disabled}
|
disabled={
|
||||||
|
this.props.stopped ||
|
||||||
|
this.props.streamMode ||
|
||||||
|
this.props.disabled
|
||||||
|
}
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
onChange={(_, value) => {
|
onChange={(_, value) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
sliderTime: value,
|
sliderTime: value,
|
||||||
sliderLock: true
|
sliderLock: true,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onChangeCommitted={() => {
|
onChangeCommitted={() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
sliderLock: false
|
sliderLock: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.handleSeek(this.state.sliderTime)
|
this.handleSeek(this.state.sliderTime)
|
||||||
@ -200,20 +202,24 @@ export default class SeekBar extends React.Component {
|
|||||||
}}
|
}}
|
||||||
valueLabelDisplay="auto"
|
valueLabelDisplay="auto"
|
||||||
valueLabelFormat={(value) => {
|
valueLabelFormat={(value) => {
|
||||||
return seekToTimeLabel((value / 100) * app.cores.player.controls.duration())
|
return seekToTimeLabel(
|
||||||
|
(value / 100) *
|
||||||
|
app.cores.player.controls.duration(),
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{!this.props.streamMode && (
|
||||||
<div className="timers">
|
<div className="timers">
|
||||||
<div>
|
<div>
|
||||||
<span>{this.state.timeText}</span>
|
<span>{this.state.timeText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{
|
<span>{this.state.durationText}</span>
|
||||||
this.props.streamMode ? <antd.Tag>Live</antd.Tag> : <span>{this.state.durationText}</span>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import classnames from "classnames"
|
|||||||
|
|
||||||
import { Icons } from "@components/Icons"
|
import { Icons } from "@components/Icons"
|
||||||
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
import { usePlayerStateContext } from "@contexts/WithPlayerContext"
|
||||||
|
import LiveInfo from "@components/Player/LiveInfo"
|
||||||
import SeekBar from "@components/Player/SeekBar"
|
import SeekBar from "@components/Player/SeekBar"
|
||||||
import Controls from "@components/Player/Controls"
|
import Controls from "@components/Player/Controls"
|
||||||
|
|
||||||
@ -25,9 +26,7 @@ function isOverflown(parent, element) {
|
|||||||
return elementRect.width > parentRect.width
|
return elementRect.width > parentRect.width
|
||||||
}
|
}
|
||||||
|
|
||||||
const Indicators = (props) => {
|
const Indicators = ({ track, playerState }) => {
|
||||||
const { track } = props
|
|
||||||
|
|
||||||
if (!track) {
|
if (!track) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -40,6 +39,12 @@ const Indicators = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (playerState.live) {
|
||||||
|
indicators.push(
|
||||||
|
<Icons.FiRadio style={{ color: "var(--colorPrimary)" }} />,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (indicators.length === 0) {
|
if (indicators.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -152,12 +157,6 @@ const Player = (props) => {
|
|||||||
onClick={() => app.location.push("/lyrics")}
|
onClick={() => app.location.push("/lyrics")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <antd.Button
|
|
||||||
icon={<Icons.MdOfflineBolt />}
|
|
||||||
>
|
|
||||||
HyperDrive
|
|
||||||
</antd.Button> */}
|
|
||||||
|
|
||||||
<antd.Button
|
<antd.Button
|
||||||
icon={<Icons.FiX />}
|
icon={<Icons.FiX />}
|
||||||
shape="circle"
|
shape="circle"
|
||||||
@ -206,20 +205,26 @@ const Player = (props) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{playerState.radioId && (
|
||||||
|
<LiveInfo radioId={playerState.radioId} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="toolbar_player_actions">
|
<div className="toolbar_player_actions">
|
||||||
<Controls />
|
<Controls streamMode={playerState.live} />
|
||||||
|
|
||||||
<SeekBar
|
<SeekBar
|
||||||
stopped={playerState.playback_status === "stopped"}
|
stopped={playerState.playback_status === "stopped"}
|
||||||
playing={playerState.playback_status === "playing"}
|
playing={playerState.playback_status === "playing"}
|
||||||
streamMode={playerState.livestream_mode}
|
streamMode={playerState.live}
|
||||||
disabled={playerState.control_locked}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ExtraActions />
|
<ExtraActions streamMode={playerState.live} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Indicators track={playerState.track_manifest} />
|
<Indicators
|
||||||
|
track={playerState.track_manifest}
|
||||||
|
playerState={playerState}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user