mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-11 03:24:16 +00:00
move player components to individual dir
This commit is contained in:
parent
a1617c2e66
commit
c962f19ff8
@ -1,549 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import * as antd from "antd"
|
|
||||||
import Slider from "@mui/material/Slider"
|
|
||||||
import classnames from "classnames"
|
|
||||||
|
|
||||||
import UseAnimations from "react-useanimations"
|
|
||||||
import LoadingAnimation from "react-useanimations/lib/loading"
|
|
||||||
|
|
||||||
import LikeButton from "components/LikeButton"
|
|
||||||
import { Icons, createIconRender } from "components/Icons"
|
|
||||||
|
|
||||||
import "./index.less"
|
|
||||||
|
|
||||||
// TODO: Queue view
|
|
||||||
const AudioVolume = (props) => {
|
|
||||||
return <div className="volumeSlider">
|
|
||||||
<antd.Slider
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.01}
|
|
||||||
value={props.volume}
|
|
||||||
onAfterChange={props.onChange}
|
|
||||||
defaultValue={props.defaultValue}
|
|
||||||
tooltip={{
|
|
||||||
formatter: (value) => {
|
|
||||||
return `${Math.round(value * 100)}%`
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
vertical
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SeekBar extends React.Component {
|
|
||||||
state = {
|
|
||||||
timeText: "00:00",
|
|
||||||
durationText: "00:00",
|
|
||||||
sliderTime: 0,
|
|
||||||
sliderLock: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSeek = (value) => {
|
|
||||||
if (value > 0) {
|
|
||||||
// calculate the duration of the audio
|
|
||||||
const duration = app.cores.player.duration()
|
|
||||||
|
|
||||||
// calculate the seek of the audio
|
|
||||||
const seek = (value / 100) * duration
|
|
||||||
|
|
||||||
app.cores.player.seek(seek)
|
|
||||||
} else {
|
|
||||||
app.cores.player.seek(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateDuration = () => {
|
|
||||||
// get current audio duration
|
|
||||||
const audioDuration = app.cores.player.duration()
|
|
||||||
|
|
||||||
if (isNaN(audioDuration)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Audio duration: ${audioDuration}`)
|
|
||||||
|
|
||||||
// set duration
|
|
||||||
this.setState({
|
|
||||||
durationText: this.seekToTimeLabel(audioDuration)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateTime = () => {
|
|
||||||
// get current audio seek
|
|
||||||
const seek = app.cores.player.seek()
|
|
||||||
|
|
||||||
// set time
|
|
||||||
this.setState({
|
|
||||||
timeText: this.seekToTimeLabel(seek)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
seekToTimeLabel = (value) => {
|
|
||||||
// convert seek to minutes and seconds
|
|
||||||
const minutes = Math.floor(value / 60)
|
|
||||||
|
|
||||||
// add leading zero if minutes is less than 10
|
|
||||||
const minutesString = minutes < 10 ? `0${minutes}` : minutes
|
|
||||||
|
|
||||||
// get seconds
|
|
||||||
const seconds = Math.floor(value - minutes * 60)
|
|
||||||
|
|
||||||
// add leading zero if seconds is less than 10
|
|
||||||
const secondsString = seconds < 10 ? `0${seconds}` : seconds
|
|
||||||
|
|
||||||
return `${minutesString}:${secondsString}`
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProgressBar = () => {
|
|
||||||
if (this.state.sliderLock) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const seek = app.cores.player.seek()
|
|
||||||
const duration = app.cores.player.duration()
|
|
||||||
|
|
||||||
const percent = (seek / duration) * 100
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
sliderTime: percent
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAll = () => {
|
|
||||||
this.calculateTime()
|
|
||||||
this.updateProgressBar()
|
|
||||||
}
|
|
||||||
|
|
||||||
events = {
|
|
||||||
"player.status.update": (status) => {
|
|
||||||
console.log(`Player status updated: ${status}`)
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case "stopped":
|
|
||||||
this.setState({
|
|
||||||
timeText: "00:00",
|
|
||||||
durationText: "00:00",
|
|
||||||
sliderTime: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
case "playing":
|
|
||||||
this.updateAll()
|
|
||||||
this.calculateDuration()
|
|
||||||
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"player.current.update": (currentAudioManifest) => {
|
|
||||||
console.log(`Player current audio updated:`, currentAudioManifest)
|
|
||||||
|
|
||||||
this.updateAll()
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
timeText: "00:00",
|
|
||||||
sliderTime: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.calculateDuration()
|
|
||||||
},
|
|
||||||
"player.duration.update": (duration) => {
|
|
||||||
console.log(`Player duration updated: ${duration}`)
|
|
||||||
|
|
||||||
this.calculateDuration()
|
|
||||||
},
|
|
||||||
"player.seek.update": (seek) => {
|
|
||||||
console.log(`Player seek updated: ${seek}`)
|
|
||||||
|
|
||||||
this.calculateTime()
|
|
||||||
this.updateAll()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tick = () => {
|
|
||||||
if (this.props.playing || this.props.streamMode) {
|
|
||||||
this.interval = setInterval(() => {
|
|
||||||
this.updateAll()
|
|
||||||
}, 1000)
|
|
||||||
} else {
|
|
||||||
if (this.interval) {
|
|
||||||
clearInterval(this.interval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount = () => {
|
|
||||||
this.calculateDuration()
|
|
||||||
this.tick()
|
|
||||||
|
|
||||||
for (const [event, callback] of Object.entries(this.events)) {
|
|
||||||
app.eventBus.on(event, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
for (const [event, callback] of Object.entries(this.events)) {
|
|
||||||
app.eventBus.off(event, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate = (prevProps, prevState) => {
|
|
||||||
if (this.props.playing !== prevProps.playing) {
|
|
||||||
this.tick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div
|
|
||||||
className={classnames(
|
|
||||||
"status",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classnames(
|
|
||||||
"progress",
|
|
||||||
{
|
|
||||||
["hidden"]: this.props.streamMode,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Slider
|
|
||||||
size="small"
|
|
||||||
value={this.state.sliderTime}
|
|
||||||
disabled={this.props.stopped || this.props.streamMode || this.props.disabled}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
step={0.1}
|
|
||||||
onChange={(_, value) => {
|
|
||||||
this.setState({
|
|
||||||
sliderTime: value,
|
|
||||||
sliderLock: true
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onChangeCommitted={() => {
|
|
||||||
this.setState({
|
|
||||||
sliderLock: false
|
|
||||||
})
|
|
||||||
|
|
||||||
this.handleSeek(this.state.sliderTime)
|
|
||||||
|
|
||||||
if (!this.props.playing) {
|
|
||||||
app.cores.player.playback.play()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
valueLabelDisplay="auto"
|
|
||||||
valueLabelFormat={(value) => {
|
|
||||||
return this.seekToTimeLabel((value / 100) * app.cores.player.duration())
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="timers">
|
|
||||||
<div>
|
|
||||||
<span>{this.state.timeText}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
this.props.streamMode ? <antd.Tag>Live</antd.Tag> : <span>{this.state.durationText}</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const AudioPlayerChangeModeButton = (props) => {
|
|
||||||
const [mode, setMode] = React.useState(app.cores.player.playback.mode())
|
|
||||||
|
|
||||||
const modeToIcon = {
|
|
||||||
"normal": "MdArrowForward",
|
|
||||||
"repeat": "MdRepeat",
|
|
||||||
"shuffle": "MdShuffle",
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClick = () => {
|
|
||||||
const modes = Object.keys(modeToIcon)
|
|
||||||
|
|
||||||
const newMode = modes[(modes.indexOf(mode) + 1) % modes.length]
|
|
||||||
|
|
||||||
app.cores.player.playback.mode(newMode)
|
|
||||||
|
|
||||||
setMode(newMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <antd.Button
|
|
||||||
icon={createIconRender(modeToIcon[mode])}
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={props.disabled ?? false}
|
|
||||||
type="ghost"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class AudioPlayer extends React.Component {
|
|
||||||
state = {
|
|
||||||
loading: app.cores.player.getState("loading") ?? false,
|
|
||||||
currentPlaying: app.cores.player.getState("currentAudioManifest"),
|
|
||||||
playbackStatus: app.cores.player.getState("playbackStatus") ?? "stopped",
|
|
||||||
audioMuted: app.cores.player.getState("audioMuted") ?? false,
|
|
||||||
audioVolume: app.cores.player.getState("audioVolume") ?? 0.3,
|
|
||||||
bpm: app.cores.player.getState("trackBPM") ?? 0,
|
|
||||||
showControls: false,
|
|
||||||
minimized: false,
|
|
||||||
streamMode: false,
|
|
||||||
|
|
||||||
syncModeLocked: app.cores.player.getState("syncModeLocked"),
|
|
||||||
syncMode: app.cores.player.getState("syncMode"),
|
|
||||||
}
|
|
||||||
|
|
||||||
events = {
|
|
||||||
"player.syncModeLocked.update": (to) => {
|
|
||||||
this.setState({ syncModeLocked: to })
|
|
||||||
},
|
|
||||||
"player.syncMode.update": (to) => {
|
|
||||||
this.setState({ syncMode: to })
|
|
||||||
},
|
|
||||||
"player.livestream.update": (data) => {
|
|
||||||
this.setState({ streamMode: data })
|
|
||||||
},
|
|
||||||
"player.bpm.update": (data) => {
|
|
||||||
this.setState({ bpm: data })
|
|
||||||
},
|
|
||||||
"player.loading.update": (data) => {
|
|
||||||
this.setState({ loading: data })
|
|
||||||
},
|
|
||||||
"player.status.update": (data) => {
|
|
||||||
this.setState({ playbackStatus: data })
|
|
||||||
},
|
|
||||||
"player.current.update": (data) => {
|
|
||||||
this.setState({ currentPlaying: data })
|
|
||||||
},
|
|
||||||
"player.mute.update": (data) => {
|
|
||||||
this.setState({ audioMuted: data })
|
|
||||||
},
|
|
||||||
"player.volume.update": (data) => {
|
|
||||||
this.setState({ audioVolume: data })
|
|
||||||
},
|
|
||||||
"player.minimized.update": (minimized) => {
|
|
||||||
this.setState({ minimized })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount = async () => {
|
|
||||||
Object.entries(this.events).forEach(([event, callback]) => {
|
|
||||||
app.eventBus.on(event, callback)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
Object.entries(this.events).forEach(([event, callback]) => {
|
|
||||||
app.eventBus.off(event, callback)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouse = (event) => {
|
|
||||||
const { type } = event
|
|
||||||
|
|
||||||
if (type === "mouseenter") {
|
|
||||||
this.setState({ showControls: true })
|
|
||||||
} else if (type === "mouseleave") {
|
|
||||||
this.setState({ showControls: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
minimize = () => {
|
|
||||||
app.cores.player.minimize()
|
|
||||||
}
|
|
||||||
|
|
||||||
close = () => {
|
|
||||||
app.cores.player.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
openVisualizer = () => {
|
|
||||||
app.setLocation("/lyrics")
|
|
||||||
}
|
|
||||||
|
|
||||||
inviteSync = () => {
|
|
||||||
app.cores.sync.music.createSyncRoom()
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVolume = (value) => {
|
|
||||||
app.cores.player.volume(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
toogleMute = () => {
|
|
||||||
app.cores.player.toogleMute()
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickPlayButton = () => {
|
|
||||||
if (this.state.streamMode) {
|
|
||||||
return app.cores.player.playback.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.cores.player.playback.toogle()
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickPreviousButton = () => {
|
|
||||||
app.cores.player.playback.previous()
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickNextButton = () => {
|
|
||||||
app.cores.player.playback.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickLikeButton = () => {
|
|
||||||
// TODO: Like
|
|
||||||
|
|
||||||
console.log("Like")
|
|
||||||
|
|
||||||
this.setState({ liked: !this.state.liked })
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
loading,
|
|
||||||
currentPlaying,
|
|
||||||
playbackStatus,
|
|
||||||
audioMuted,
|
|
||||||
audioVolume,
|
|
||||||
} = this.state
|
|
||||||
|
|
||||||
return <div
|
|
||||||
className={classnames(
|
|
||||||
"embbededMediaPlayerWrapper",
|
|
||||||
{
|
|
||||||
["hovering"]: this.state.showControls,
|
|
||||||
["minimized"]: this.state.minimized,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
onMouseEnter={this.onMouse}
|
|
||||||
onMouseLeave={this.onMouse}
|
|
||||||
>
|
|
||||||
<div className="top_controls">
|
|
||||||
<antd.Button
|
|
||||||
icon={<Icons.MdFirstPage />}
|
|
||||||
onClick={this.minimize}
|
|
||||||
shape="circle"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
!this.state.syncModeLocked && !this.state.syncMode && <antd.Button
|
|
||||||
icon={<Icons.MdShare />}
|
|
||||||
onClick={this.inviteSync}
|
|
||||||
shape="circle"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<antd.Button
|
|
||||||
icon={<Icons.MdOpenInFull />}
|
|
||||||
onClick={this.openVisualizer}
|
|
||||||
shape="circle"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<antd.Button
|
|
||||||
className="bottom_btn"
|
|
||||||
icon={<Icons.X />}
|
|
||||||
onClick={this.close}
|
|
||||||
shape="square"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="player">
|
|
||||||
<div
|
|
||||||
className="cover"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url(${(currentPlaying?.thumbnail) ?? "/assets/no_song.png"})`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="header">
|
|
||||||
<div className="info">
|
|
||||||
<div className="title">
|
|
||||||
<h2>
|
|
||||||
{
|
|
||||||
currentPlaying?.title
|
|
||||||
? currentPlaying?.title
|
|
||||||
: (loading ? "Loading..." : (currentPlaying?.title ?? "Untitled"))
|
|
||||||
}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="subTitle">
|
|
||||||
{
|
|
||||||
currentPlaying?.artist && <div className="artist">
|
|
||||||
<h3>
|
|
||||||
{currentPlaying?.artist ?? "Unknown"}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<LikeButton
|
|
||||||
onClick={this.onClickLikeButton}
|
|
||||||
liked={this.state.liked}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="controls">
|
|
||||||
<AudioPlayerChangeModeButton
|
|
||||||
disabled={this.state.syncModeLocked}
|
|
||||||
/>
|
|
||||||
<antd.Button
|
|
||||||
type="ghost"
|
|
||||||
shape="round"
|
|
||||||
icon={<Icons.ChevronLeft />}
|
|
||||||
onClick={this.onClickPreviousButton}
|
|
||||||
disabled={this.state.syncModeLocked}
|
|
||||||
/>
|
|
||||||
<antd.Button
|
|
||||||
type="primary"
|
|
||||||
shape="circle"
|
|
||||||
icon={this.state.streamMode ? <Icons.MdStop /> : playbackStatus === "playing" ? <Icons.MdPause /> : <Icons.MdPlayArrow />}
|
|
||||||
onClick={this.onClickPlayButton}
|
|
||||||
className="playButton"
|
|
||||||
disabled={this.state.syncModeLocked}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
loading && <div className="loadCircle">
|
|
||||||
<UseAnimations
|
|
||||||
animation={LoadingAnimation}
|
|
||||||
size="100%"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</antd.Button>
|
|
||||||
<antd.Button
|
|
||||||
type="ghost"
|
|
||||||
shape="round"
|
|
||||||
icon={<Icons.ChevronRight />}
|
|
||||||
onClick={this.onClickNextButton}
|
|
||||||
disabled={this.state.syncModeLocked}
|
|
||||||
/>
|
|
||||||
<antd.Popover
|
|
||||||
content={React.createElement(
|
|
||||||
AudioVolume,
|
|
||||||
{ onChange: this.updateVolume, defaultValue: audioVolume }
|
|
||||||
)}
|
|
||||||
trigger="hover"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="muteButton"
|
|
||||||
onClick={this.toogleMute}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
audioMuted
|
|
||||||
? <Icons.VolumeX />
|
|
||||||
: <Icons.Volume2 />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</antd.Popover>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SeekBar
|
|
||||||
stopped={playbackStatus === "stopped"}
|
|
||||||
playing={playbackStatus === "playing"}
|
|
||||||
streamMode={this.state.streamMode}
|
|
||||||
disabled={this.state.syncModeLocked}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
23
packages/app/src/components/Player/AudioVolume/index.jsx
Normal file
23
packages/app/src/components/Player/AudioVolume/index.jsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
return <div className="player-volume_slider">
|
||||||
|
<antd.Slider
|
||||||
|
min={0}
|
||||||
|
max={1}
|
||||||
|
step={0.01}
|
||||||
|
value={props.volume}
|
||||||
|
onAfterChange={props.onChange}
|
||||||
|
defaultValue={props.defaultValue}
|
||||||
|
tooltip={{
|
||||||
|
formatter: (value) => {
|
||||||
|
return `${Math.round(value * 100)}%`
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
vertical
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
14
packages/app/src/components/Player/AudioVolume/index.less
Normal file
14
packages/app/src/components/Player/AudioVolume/index.less
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
.ant-popover-inner-content {
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.player-volume_slider {
|
||||||
|
height: 150px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
|
||||||
|
import { Icons, createIconRender } from "components/Icons"
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
const [mode, setMode] = React.useState(app.cores.player.playback.mode())
|
||||||
|
|
||||||
|
const modeToIcon = {
|
||||||
|
"normal": "MdArrowForward",
|
||||||
|
"repeat": "MdRepeat",
|
||||||
|
"shuffle": "MdShuffle",
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
const modes = Object.keys(modeToIcon)
|
||||||
|
|
||||||
|
const newMode = modes[(modes.indexOf(mode) + 1) % modes.length]
|
||||||
|
|
||||||
|
app.cores.player.playback.mode(newMode)
|
||||||
|
|
||||||
|
setMode(newMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <antd.Button
|
||||||
|
icon={createIconRender(modeToIcon[mode])}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={props.disabled ?? false}
|
||||||
|
type="ghost"
|
||||||
|
/>
|
||||||
|
}
|
101
packages/app/src/components/Player/Controls/index.jsx
Normal file
101
packages/app/src/components/Player/Controls/index.jsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
|
||||||
|
import UseAnimations from "react-useanimations"
|
||||||
|
import LoadingAnimation from "react-useanimations/lib/loading"
|
||||||
|
|
||||||
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
|
import AudioVolume from "components/Player/AudioVolume"
|
||||||
|
import AudioPlayerChangeModeButton from "components/Player/ChangeModeButton"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default ({
|
||||||
|
className,
|
||||||
|
controls,
|
||||||
|
syncModeLocked = false,
|
||||||
|
syncMode = false,
|
||||||
|
streamMode,
|
||||||
|
playbackStatus,
|
||||||
|
onVolumeUpdate,
|
||||||
|
onMuteUpdate,
|
||||||
|
audioVolume = 0.3,
|
||||||
|
audioMuted = false,
|
||||||
|
loading = false,
|
||||||
|
} = {}) => {
|
||||||
|
const onClickActionsButton = (event) => {
|
||||||
|
if (typeof controls !== "object") {
|
||||||
|
console.warn("[AudioPlayer] onClickActionsButton: props.controls is not an object")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof controls[event] !== "function") {
|
||||||
|
console.warn(`[AudioPlayer] onClickActionsButton: ${event} is not a function`)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return controls[event]()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className={
|
||||||
|
className ?? "player-controls"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AudioPlayerChangeModeButton
|
||||||
|
disabled={syncModeLocked}
|
||||||
|
/>
|
||||||
|
<antd.Button
|
||||||
|
type="ghost"
|
||||||
|
shape="round"
|
||||||
|
icon={<Icons.ChevronLeft />}
|
||||||
|
onClick={() => onClickActionsButton("previous")}
|
||||||
|
disabled={syncModeLocked}
|
||||||
|
/>
|
||||||
|
<antd.Button
|
||||||
|
type="primary"
|
||||||
|
shape="circle"
|
||||||
|
icon={streamMode ? <Icons.MdStop /> : playbackStatus === "playing" ? <Icons.MdPause /> : <Icons.MdPlayArrow />}
|
||||||
|
onClick={() => onClickActionsButton("toogle")}
|
||||||
|
className="playButton"
|
||||||
|
disabled={syncModeLocked}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
loading && <div className="loadCircle">
|
||||||
|
<UseAnimations
|
||||||
|
animation={LoadingAnimation}
|
||||||
|
size="100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</antd.Button>
|
||||||
|
<antd.Button
|
||||||
|
type="ghost"
|
||||||
|
shape="round"
|
||||||
|
icon={<Icons.ChevronRight />}
|
||||||
|
onClick={() => onClickActionsButton("next")}
|
||||||
|
disabled={syncModeLocked}
|
||||||
|
/>
|
||||||
|
<antd.Popover
|
||||||
|
content={React.createElement(
|
||||||
|
AudioVolume,
|
||||||
|
{ onChange: onVolumeUpdate, defaultValue: audioVolume }
|
||||||
|
)}
|
||||||
|
trigger="hover"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="muteButton"
|
||||||
|
onClick={onMuteUpdate}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
audioMuted
|
||||||
|
? <Icons.VolumeX />
|
||||||
|
: <Icons.Volume2 />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</antd.Popover>
|
||||||
|
</div>
|
||||||
|
}
|
61
packages/app/src/components/Player/Controls/index.less
Normal file
61
packages/app/src/components/Player/Controls/index.less
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
.player-controls {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playButton {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.loadCircle {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
z-index: 330;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
justify-self: center;
|
||||||
|
|
||||||
|
transform: scale(1.5);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
path {
|
||||||
|
stroke: var(--text-color);
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.muteButton {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
240
packages/app/src/components/Player/MediaPlayer/index.jsx
Executable file
240
packages/app/src/components/Player/MediaPlayer/index.jsx
Executable file
@ -0,0 +1,240 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import classnames from "classnames"
|
||||||
|
|
||||||
|
import LikeButton from "components/LikeButton"
|
||||||
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
|
import SeekBar from "components/Player/SeekBar"
|
||||||
|
import Controls from "components/Player/Controls"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
// TODO: Queue view
|
||||||
|
export default class AudioPlayer extends React.Component {
|
||||||
|
state = {
|
||||||
|
loading: app.cores.player.getState("loading") ?? false,
|
||||||
|
currentPlaying: app.cores.player.getState("currentAudioManifest"),
|
||||||
|
playbackStatus: app.cores.player.getState("playbackStatus") ?? "stopped",
|
||||||
|
audioMuted: app.cores.player.getState("audioMuted") ?? false,
|
||||||
|
audioVolume: app.cores.player.getState("audioVolume") ?? 0.3,
|
||||||
|
bpm: app.cores.player.getState("trackBPM") ?? 0,
|
||||||
|
showControls: false,
|
||||||
|
minimized: false,
|
||||||
|
streamMode: false,
|
||||||
|
|
||||||
|
syncModeLocked: app.cores.player.getState("syncModeLocked"),
|
||||||
|
syncMode: app.cores.player.getState("syncMode"),
|
||||||
|
}
|
||||||
|
|
||||||
|
events = {
|
||||||
|
"player.syncModeLocked.update": (to) => {
|
||||||
|
this.setState({ syncModeLocked: to })
|
||||||
|
},
|
||||||
|
"player.syncMode.update": (to) => {
|
||||||
|
this.setState({ syncMode: to })
|
||||||
|
},
|
||||||
|
"player.livestream.update": (data) => {
|
||||||
|
this.setState({ streamMode: data })
|
||||||
|
},
|
||||||
|
"player.bpm.update": (data) => {
|
||||||
|
this.setState({ bpm: data })
|
||||||
|
},
|
||||||
|
"player.loading.update": (data) => {
|
||||||
|
this.setState({ loading: data })
|
||||||
|
},
|
||||||
|
"player.status.update": (data) => {
|
||||||
|
this.setState({ playbackStatus: data })
|
||||||
|
},
|
||||||
|
"player.current.update": (data) => {
|
||||||
|
this.setState({ currentPlaying: data })
|
||||||
|
},
|
||||||
|
"player.mute.update": (data) => {
|
||||||
|
this.setState({ audioMuted: data })
|
||||||
|
},
|
||||||
|
"player.volume.update": (data) => {
|
||||||
|
this.setState({ audioVolume: data })
|
||||||
|
},
|
||||||
|
"player.minimized.update": (minimized) => {
|
||||||
|
this.setState({ minimized })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
Object.entries(this.events).forEach(([event, callback]) => {
|
||||||
|
app.eventBus.on(event, callback)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
Object.entries(this.events).forEach(([event, callback]) => {
|
||||||
|
app.eventBus.off(event, callback)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouse = (event) => {
|
||||||
|
const { type } = event
|
||||||
|
|
||||||
|
if (type === "mouseenter") {
|
||||||
|
this.setState({ showControls: true })
|
||||||
|
} else if (type === "mouseleave") {
|
||||||
|
this.setState({ showControls: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minimize = () => {
|
||||||
|
app.cores.player.minimize()
|
||||||
|
}
|
||||||
|
|
||||||
|
close = () => {
|
||||||
|
app.cores.player.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
openVisualizer = () => {
|
||||||
|
app.setLocation("/lyrics")
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteSync = () => {
|
||||||
|
app.cores.sync.music.createSyncRoom()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVolume = (value) => {
|
||||||
|
app.cores.player.volume(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
toogleMute = () => {
|
||||||
|
app.cores.player.toogleMute()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickPlayButton = () => {
|
||||||
|
if (this.state.streamMode) {
|
||||||
|
return app.cores.player.playback.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
app.cores.player.playback.toogle()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickPreviousButton = () => {
|
||||||
|
app.cores.player.playback.previous()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickNextButton = () => {
|
||||||
|
app.cores.player.playback.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickLikeButton = () => {
|
||||||
|
// TODO: Like
|
||||||
|
|
||||||
|
console.log("Like")
|
||||||
|
|
||||||
|
this.setState({ liked: !this.state.liked })
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
currentPlaying,
|
||||||
|
playbackStatus,
|
||||||
|
audioMuted,
|
||||||
|
audioVolume,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className={classnames(
|
||||||
|
"embbededMediaPlayerWrapper",
|
||||||
|
{
|
||||||
|
["hovering"]: this.state.showControls,
|
||||||
|
["minimized"]: this.state.minimized,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onMouseEnter={this.onMouse}
|
||||||
|
onMouseLeave={this.onMouse}
|
||||||
|
>
|
||||||
|
<div className="top_controls">
|
||||||
|
<antd.Button
|
||||||
|
icon={<Icons.MdFirstPage />}
|
||||||
|
onClick={this.minimize}
|
||||||
|
shape="circle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!this.state.syncModeLocked && !this.state.syncMode && <antd.Button
|
||||||
|
icon={<Icons.MdShare />}
|
||||||
|
onClick={this.inviteSync}
|
||||||
|
shape="circle"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<antd.Button
|
||||||
|
icon={<Icons.MdOpenInFull />}
|
||||||
|
onClick={this.openVisualizer}
|
||||||
|
shape="circle"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<antd.Button
|
||||||
|
className="bottom_btn"
|
||||||
|
icon={<Icons.X />}
|
||||||
|
onClick={this.close}
|
||||||
|
shape="square"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="player">
|
||||||
|
<div
|
||||||
|
className="cover"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${(currentPlaying?.thumbnail) ?? "/assets/no_song.png"})`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="header">
|
||||||
|
<div className="info">
|
||||||
|
<div className="title">
|
||||||
|
<h2>
|
||||||
|
{
|
||||||
|
currentPlaying?.title
|
||||||
|
? currentPlaying?.title
|
||||||
|
: (loading ? "Loading..." : (currentPlaying?.title ?? "Untitled"))
|
||||||
|
}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="subTitle">
|
||||||
|
{
|
||||||
|
currentPlaying?.artist && <div className="artist">
|
||||||
|
<h3>
|
||||||
|
{currentPlaying?.artist ?? "Unknown"}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<LikeButton
|
||||||
|
onClick={this.onClickLikeButton}
|
||||||
|
liked={this.state.liked}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Controls
|
||||||
|
syncModeLocked={this.state.syncModeLocked}
|
||||||
|
syncMode={this.state.syncMode}
|
||||||
|
playbackStatus={playbackStatus}
|
||||||
|
audioMuted={audioMuted}
|
||||||
|
audioVolume={audioVolume}
|
||||||
|
onVolumeUpdate={this.updateVolume}
|
||||||
|
onMuteUpdate={this.toogleMute}
|
||||||
|
controls={{
|
||||||
|
previous: this.onClickPreviousButton,
|
||||||
|
toogle: this.onClickPlayButton,
|
||||||
|
next: this.onClickNextButton,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SeekBar
|
||||||
|
stopped={playbackStatus === "stopped"}
|
||||||
|
playing={playbackStatus === "playing"}
|
||||||
|
streamMode={this.state.streamMode}
|
||||||
|
disabled={this.state.syncModeLocked}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
@ -177,145 +177,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
color: var(--text-color);
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playButton {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.loadCircle {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
z-index: 330;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
align-self: center;
|
|
||||||
justify-self: center;
|
|
||||||
|
|
||||||
transform: scale(1.5);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
path {
|
|
||||||
stroke: var(--text-color);
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.muteButton {
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
z-index: 330;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
align-self: center;
|
|
||||||
|
|
||||||
width: 90%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
margin: 20px 0 10px 0;
|
|
||||||
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
height: 0px;
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
opacity: 0;
|
|
||||||
height: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.timers {
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: fit-content;
|
|
||||||
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6,
|
|
||||||
p,
|
|
||||||
span {
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-popover-inner-content {
|
|
||||||
height: fit-content;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.volumeSlider {
|
|
||||||
height: 250px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
228
packages/app/src/components/Player/SeekBar/index.jsx
Normal file
228
packages/app/src/components/Player/SeekBar/index.jsx
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import Slider from "@mui/material/Slider"
|
||||||
|
import classnames from "classnames"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
export default class SeekBar extends React.Component {
|
||||||
|
state = {
|
||||||
|
timeText: "00:00",
|
||||||
|
durationText: "00:00",
|
||||||
|
sliderTime: 0,
|
||||||
|
sliderLock: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSeek = (value) => {
|
||||||
|
if (value > 0) {
|
||||||
|
// calculate the duration of the audio
|
||||||
|
const duration = app.cores.player.duration()
|
||||||
|
|
||||||
|
// calculate the seek of the audio
|
||||||
|
const seek = (value / 100) * duration
|
||||||
|
|
||||||
|
app.cores.player.seek(seek)
|
||||||
|
} else {
|
||||||
|
app.cores.player.seek(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateDuration = () => {
|
||||||
|
// get current audio duration
|
||||||
|
const audioDuration = app.cores.player.duration()
|
||||||
|
|
||||||
|
if (isNaN(audioDuration)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Audio duration: ${audioDuration}`)
|
||||||
|
|
||||||
|
// set duration
|
||||||
|
this.setState({
|
||||||
|
durationText: this.seekToTimeLabel(audioDuration)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateTime = () => {
|
||||||
|
// get current audio seek
|
||||||
|
const seek = app.cores.player.seek()
|
||||||
|
|
||||||
|
// set time
|
||||||
|
this.setState({
|
||||||
|
timeText: this.seekToTimeLabel(seek)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
seekToTimeLabel = (value) => {
|
||||||
|
// convert seek to minutes and seconds
|
||||||
|
const minutes = Math.floor(value / 60)
|
||||||
|
|
||||||
|
// add leading zero if minutes is less than 10
|
||||||
|
const minutesString = minutes < 10 ? `0${minutes}` : minutes
|
||||||
|
|
||||||
|
// get seconds
|
||||||
|
const seconds = Math.floor(value - minutes * 60)
|
||||||
|
|
||||||
|
// add leading zero if seconds is less than 10
|
||||||
|
const secondsString = seconds < 10 ? `0${seconds}` : seconds
|
||||||
|
|
||||||
|
return `${minutesString}:${secondsString}`
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgressBar = () => {
|
||||||
|
if (this.state.sliderLock) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const seek = app.cores.player.seek()
|
||||||
|
const duration = app.cores.player.duration()
|
||||||
|
|
||||||
|
const percent = (seek / duration) * 100
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
sliderTime: percent
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAll = () => {
|
||||||
|
this.calculateTime()
|
||||||
|
this.updateProgressBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
events = {
|
||||||
|
"player.status.update": (status) => {
|
||||||
|
console.log(`Player status updated: ${status}`)
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "stopped":
|
||||||
|
this.setState({
|
||||||
|
timeText: "00:00",
|
||||||
|
durationText: "00:00",
|
||||||
|
sliderTime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
break
|
||||||
|
case "playing":
|
||||||
|
this.updateAll()
|
||||||
|
this.calculateDuration()
|
||||||
|
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"player.current.update": (currentAudioManifest) => {
|
||||||
|
console.log(`Player current audio updated:`, currentAudioManifest)
|
||||||
|
|
||||||
|
this.updateAll()
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
timeText: "00:00",
|
||||||
|
sliderTime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.calculateDuration()
|
||||||
|
},
|
||||||
|
"player.duration.update": (duration) => {
|
||||||
|
console.log(`Player duration updated: ${duration}`)
|
||||||
|
|
||||||
|
this.calculateDuration()
|
||||||
|
},
|
||||||
|
"player.seek.update": (seek) => {
|
||||||
|
console.log(`Player seek updated: ${seek}`)
|
||||||
|
|
||||||
|
this.calculateTime()
|
||||||
|
this.updateAll()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tick = () => {
|
||||||
|
if (this.props.playing || this.props.streamMode) {
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.updateAll()
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
this.calculateDuration()
|
||||||
|
this.tick()
|
||||||
|
|
||||||
|
for (const [event, callback] of Object.entries(this.events)) {
|
||||||
|
app.eventBus.on(event, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount = () => {
|
||||||
|
for (const [event, callback] of Object.entries(this.events)) {
|
||||||
|
app.eventBus.off(event, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate = (prevProps, prevState) => {
|
||||||
|
if (this.props.playing !== prevProps.playing) {
|
||||||
|
this.tick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div
|
||||||
|
className={classnames(
|
||||||
|
"player-seek_bar",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classnames(
|
||||||
|
"progress",
|
||||||
|
{
|
||||||
|
["hidden"]: this.props.streamMode,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Slider
|
||||||
|
size="small"
|
||||||
|
value={this.state.sliderTime}
|
||||||
|
disabled={this.props.stopped || this.props.streamMode || this.props.disabled}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={0.1}
|
||||||
|
onChange={(_, value) => {
|
||||||
|
this.setState({
|
||||||
|
sliderTime: value,
|
||||||
|
sliderLock: true
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onChangeCommitted={() => {
|
||||||
|
this.setState({
|
||||||
|
sliderLock: false
|
||||||
|
})
|
||||||
|
|
||||||
|
this.handleSeek(this.state.sliderTime)
|
||||||
|
|
||||||
|
if (!this.props.playing) {
|
||||||
|
app.cores.player.playback.play()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
valueLabelDisplay="auto"
|
||||||
|
valueLabelFormat={(value) => {
|
||||||
|
return this.seekToTimeLabel((value / 100) * app.cores.player.duration())
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="timers">
|
||||||
|
<div>
|
||||||
|
<span>{this.state.timeText}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
this.props.streamMode ? <antd.Tag>Live</antd.Tag> : <span>{this.state.durationText}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
61
packages/app/src/components/Player/SeekBar/index.less
Normal file
61
packages/app/src/components/Player/SeekBar/index.less
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
.player-seek_bar {
|
||||||
|
z-index: 330;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
width: 90%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
margin: 20px 0 10px 0;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
height: 0px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
opacity: 0;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timers {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
span {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user