mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-11 19:44:15 +00:00
improve Media player
This commit is contained in:
parent
79565eb041
commit
ec76683b4d
@ -1,12 +1,12 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
|
import Slider from "@mui/material/Slider"
|
||||||
|
import classnames from "classnames"
|
||||||
|
|
||||||
import { Icons } from "components/Icons"
|
import { Icons, createIconRender } from "components/Icons"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
// FIXME: JS events are not working properly, the methods cause a memory leak
|
|
||||||
// FIXME: Use a better way to preload audios
|
|
||||||
// TODO: Check AUDIO quality and show a quality indicator
|
// TODO: Check AUDIO quality and show a quality indicator
|
||||||
// TODO: Add close button
|
// TODO: Add close button
|
||||||
// TODO: Add repeat & shuffle mode
|
// TODO: Add repeat & shuffle mode
|
||||||
@ -31,14 +31,33 @@ const AudioVolume = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlayerStatus = React.memo((props) => {
|
class SeekBar extends React.Component {
|
||||||
const [playing, setPlaying] = React.useState(false)
|
state = {
|
||||||
const [time, setTime] = React.useState("00:00")
|
timeText: "00:00",
|
||||||
const [duration, setDuration] = React.useState("00:00")
|
durationText: "00:00",
|
||||||
const [progressBar, setProgressBar] = React.useState(0)
|
sliderTime: 0,
|
||||||
|
sliderLock: false,
|
||||||
|
}
|
||||||
|
|
||||||
const updateDuration = () => {
|
handleSeek = (value) => {
|
||||||
const audioDuration = app.AudioPlayer.currentAudio.instance.duration()
|
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()
|
||||||
|
|
||||||
|
console.log(`Audio duration: ${audioDuration}`)
|
||||||
|
|
||||||
// convert duration to minutes and seconds
|
// convert duration to minutes and seconds
|
||||||
const minutes = Math.floor(audioDuration / 60)
|
const minutes = Math.floor(audioDuration / 60)
|
||||||
@ -53,12 +72,14 @@ const PlayerStatus = React.memo((props) => {
|
|||||||
const secondsString = seconds < 10 ? `0${seconds}` : seconds
|
const secondsString = seconds < 10 ? `0${seconds}` : seconds
|
||||||
|
|
||||||
// set duration
|
// set duration
|
||||||
setDuration(`${minutesString}:${secondsString}`)
|
this.setState({
|
||||||
|
durationText: `${minutesString}:${secondsString}`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateTimer = () => {
|
calculateTime = () => {
|
||||||
// get audio seek
|
// get current audio seek
|
||||||
const seek = app.AudioPlayer.currentAudio.instance.seek()
|
const seek = app.cores.player.seek()
|
||||||
|
|
||||||
// convert seek to minutes and seconds
|
// convert seek to minutes and seconds
|
||||||
const minutes = Math.floor(seek / 60)
|
const minutes = Math.floor(seek / 60)
|
||||||
@ -73,272 +94,360 @@ const PlayerStatus = React.memo((props) => {
|
|||||||
const secondsString = seconds < 10 ? `0${seconds}` : seconds
|
const secondsString = seconds < 10 ? `0${seconds}` : seconds
|
||||||
|
|
||||||
// set time
|
// set time
|
||||||
setTime(`${minutesString}:${secondsString}`)
|
this.setState({
|
||||||
|
timeText: `${minutesString}:${secondsString}`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateProgressBar = () => {
|
updateProgressBar = () => {
|
||||||
const seek = app.AudioPlayer.currentAudio.instance.seek()
|
if (this.state.sliderLock) {
|
||||||
const duration = app.AudioPlayer.currentAudio.instance.duration()
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const seek = app.cores.player.seek()
|
||||||
|
const duration = app.cores.player.duration()
|
||||||
|
|
||||||
const percent = (seek / duration) * 100
|
const percent = (seek / duration) * 100
|
||||||
|
|
||||||
setProgressBar(percent)
|
this.setState({
|
||||||
|
sliderTime: percent
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpdateSeek = (value) => {
|
updateAll = () => {
|
||||||
// calculate the duration of the audio
|
this.calculateTime()
|
||||||
const duration = app.AudioPlayer.currentAudio.instance.duration()
|
this.updateProgressBar()
|
||||||
|
|
||||||
// calculate the seek of the audio
|
|
||||||
const seek = (value / 100) * duration
|
|
||||||
|
|
||||||
// update the progress bar
|
|
||||||
setProgressBar(value)
|
|
||||||
|
|
||||||
// seek to the new value
|
|
||||||
app.AudioPlayer.currentAudio.instance.seek(seek)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tooltipFormatter = (value) => {
|
events = {
|
||||||
if (!app.AudioPlayer.currentAudio) {
|
"player.status.update": (status) => {
|
||||||
return "00:00"
|
console.log(`Player status updated: ${status}`)
|
||||||
}
|
|
||||||
|
|
||||||
const duration = app.AudioPlayer.currentAudio.instance.duration()
|
switch (status) {
|
||||||
|
case "stopped":
|
||||||
|
this.setState({
|
||||||
|
timeText: "00:00",
|
||||||
|
durationText: "00:00",
|
||||||
|
sliderTime: 0,
|
||||||
|
})
|
||||||
|
|
||||||
const seek = (value / 100) * duration
|
break
|
||||||
|
case "playing":
|
||||||
|
this.updateAll()
|
||||||
|
this.calculateDuration()
|
||||||
|
|
||||||
// convert seek to minutes and seconds
|
break
|
||||||
const minutes = Math.floor(seek / 60)
|
default:
|
||||||
|
break
|
||||||
// add leading zero if minutes is less than 10
|
|
||||||
const minutesString = minutes < 10 ? `0${minutes}` : minutes
|
|
||||||
|
|
||||||
// get seconds
|
|
||||||
const seconds = Math.floor(seek - minutes * 60)
|
|
||||||
|
|
||||||
// add leading zero if seconds is less than 10
|
|
||||||
const secondsString = seconds < 10 ? `0${seconds}` : seconds
|
|
||||||
|
|
||||||
return `${minutesString}:${secondsString}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a interval when audio is playing, and destroy it when audio is paused
|
|
||||||
React.useEffect(() => {
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (app.AudioPlayer.currentAudio) {
|
|
||||||
updateTimer()
|
|
||||||
updateProgressBar()
|
|
||||||
}
|
}
|
||||||
}, 1000)
|
},
|
||||||
|
"player.current.update": (currentAudioManifest) => {
|
||||||
|
console.log(`Player current audio updated:`, currentAudioManifest)
|
||||||
|
|
||||||
return () => {
|
this.updateAll()
|
||||||
clearInterval(interval)
|
|
||||||
}
|
|
||||||
}, [playing])
|
|
||||||
|
|
||||||
const events = {
|
this.setState({
|
||||||
"audioPlayer.seeked": () => {
|
timeText: "00:00",
|
||||||
updateTimer()
|
sliderTime: 0,
|
||||||
updateProgressBar()
|
})
|
||||||
|
|
||||||
|
this.calculateDuration()
|
||||||
},
|
},
|
||||||
"audioPlayer.playing": () => {
|
"player.duration.update": (duration) => {
|
||||||
updateDuration()
|
console.log(`Player duration updated: ${duration}`)
|
||||||
updateTimer()
|
|
||||||
updateProgressBar()
|
this.calculateDuration()
|
||||||
},
|
},
|
||||||
"audioPlayer.paused": () => {
|
"player.seek.update": (seek) => {
|
||||||
setPlaying(false)
|
console.log(`Player seek updated: ${seek}`)
|
||||||
},
|
|
||||||
"audioPlayer.stopped": () => {
|
this.calculateTime()
|
||||||
setPlaying(false)
|
this.updateAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
tick = () => {
|
||||||
// listen events
|
if (this.props.playing) {
|
||||||
for (const [event, callback] of Object.entries(events)) {
|
this.interval = setInterval(() => {
|
||||||
|
this.updateAll()
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
this.calculateDuration()
|
||||||
|
this.tick()
|
||||||
|
|
||||||
|
for (const [event, callback] of Object.entries(this.events)) {
|
||||||
app.eventBus.on(event, callback)
|
app.eventBus.on(event, callback)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
componentWillUnmount = () => {
|
||||||
// remove events
|
for (const [event, callback] of Object.entries(this.events)) {
|
||||||
for (const [event, callback] of Object.entries(events)) {
|
app.eventBus.off(event, callback)
|
||||||
app.eventBus.off(event, callback)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [])
|
}
|
||||||
|
|
||||||
return <div className="status">
|
componentDidUpdate = (prevProps, prevState) => {
|
||||||
<div className="progress">
|
if (this.props.playing !== prevProps.playing) {
|
||||||
<antd.Slider
|
this.tick()
|
||||||
value={progressBar}
|
}
|
||||||
onAfterChange={onUpdateSeek}
|
}
|
||||||
tooltip={{ formatter: tooltipFormatter }}
|
|
||||||
disabled={!app.AudioPlayer.currentAudio}
|
render() {
|
||||||
/>
|
return <div className="status">
|
||||||
</div>
|
<div className="progress">
|
||||||
<div className="timers">
|
<Slider
|
||||||
<div>
|
size="small"
|
||||||
<span>{time}</span>
|
value={this.state.sliderTime}
|
||||||
|
disabled={this.props.stopped}
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="timers">
|
||||||
<span>{duration}</span>
|
<div>
|
||||||
|
<span>{this.state.timeText}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{this.state.durationText}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
export default (props) => {
|
const AudioPlayerChangeModeButton = (props) => {
|
||||||
const [mute, setMute] = React.useState(app.AudioPlayer.audioMuted)
|
const [mode, setMode] = React.useState(app.cores.player.playback.mode())
|
||||||
const [volume, setVolume] = React.useState(app.AudioPlayer.audioVolume)
|
|
||||||
|
|
||||||
const [currentPlaying, setCurrentPlaying] = React.useState(null)
|
const modeToIcon = {
|
||||||
const [playing, setPlaying] = React.useState(false)
|
"normal": "MdArrowForward",
|
||||||
const [loading, setLoading] = React.useState(true)
|
"repeat": "MdRepeat",
|
||||||
|
"shuffle": "MdShuffle",
|
||||||
const toogleMute = () => {
|
|
||||||
setMute(app.AudioPlayer.toogleMute())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateVolume = (value) => {
|
const onClick = () => {
|
||||||
console.log("Updating volume", value)
|
const modes = Object.keys(modeToIcon)
|
||||||
|
|
||||||
setVolume(app.AudioPlayer.setVolume(value))
|
const newMode = modes[(modes.indexOf(mode) + 1) % modes.length]
|
||||||
|
|
||||||
if (value > 0) {
|
app.cores.player.playback.mode(newMode)
|
||||||
setMute(false)
|
|
||||||
}
|
setMode(newMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickPlayButton = () => {
|
return <antd.Button
|
||||||
setPlaying(!playing)
|
icon={createIconRender(modeToIcon[mode])}
|
||||||
|
onClick={onClick}
|
||||||
|
type="ghost"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
if (playing) {
|
export default class AudioPlayer extends React.Component {
|
||||||
app.AudioPlayer.pauseAudioQueue()
|
state = {
|
||||||
} else {
|
loading: app.cores.player.getState("loading") ?? false,
|
||||||
app.AudioPlayer.playCurrentAudio()
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickNextButton = () => {
|
events = {
|
||||||
app.AudioPlayer.nextAudio()
|
"player.bpm.update": (data) => {
|
||||||
}
|
this.setState({ bpm: data })
|
||||||
|
|
||||||
const onClickPreviousButton = () => {
|
|
||||||
app.AudioPlayer.previousAudio()
|
|
||||||
}
|
|
||||||
|
|
||||||
const busEvents = {
|
|
||||||
"audioPlayer.playing": (data) => {
|
|
||||||
if (data) {
|
|
||||||
setCurrentPlaying(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false)
|
|
||||||
setPlaying(true)
|
|
||||||
},
|
},
|
||||||
"audioPlayer.pause": () => {
|
"player.loading.update": (data) => {
|
||||||
setPlaying(false)
|
this.setState({ loading: data })
|
||||||
},
|
},
|
||||||
"audioPlayer.stopped": () => {
|
"player.status.update": (data) => {
|
||||||
setPlaying(false)
|
this.setState({ playbackStatus: data })
|
||||||
},
|
},
|
||||||
"audioPlayer.loaded": () => {
|
"player.current.update": (data) => {
|
||||||
setLoading(false)
|
this.setState({ currentPlaying: data })
|
||||||
},
|
},
|
||||||
"audioPlayer.loading": () => {
|
"player.mute.update": (data) => {
|
||||||
setLoading(true)
|
this.setState({ audioMuted: data })
|
||||||
|
},
|
||||||
|
"player.volume.update": (data) => {
|
||||||
|
this.setState({ audioVolume: data })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// listen to events
|
minimize = () => {
|
||||||
React.useEffect(() => {
|
|
||||||
for (const event in busEvents) {
|
|
||||||
app.eventBus.on(event, busEvents[event])
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
}
|
||||||
for (const event in busEvents) {
|
|
||||||
app.eventBus.off(event, busEvents[event])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
console.log(currentPlaying)
|
updateVolume = (value) => {
|
||||||
|
app.cores.player.volume(value)
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="embbededMediaPlayerWrapper">
|
toogleMute = (to) => {
|
||||||
<div
|
app.cores.player.toogleMute(to)
|
||||||
className="cover"
|
}
|
||||||
style={{
|
|
||||||
backgroundImage: `url(${(currentPlaying?.cover) ?? "/assets/no_song.png"})`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="player">
|
onClickPlayButton = () => {
|
||||||
<div className="header">
|
app.cores.player.playback.toogle()
|
||||||
<div className="info">
|
}
|
||||||
<div className="title">
|
|
||||||
<h2>
|
onClickPreviousButton = () => {
|
||||||
{loading ? "Loading..." : (currentPlaying?.title ?? "Untitled")}
|
app.cores.player.playback.previous()
|
||||||
</h2>
|
}
|
||||||
|
|
||||||
|
onClickNextButton = () => {
|
||||||
|
app.cores.player.playback.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
currentPlaying,
|
||||||
|
playbackStatus,
|
||||||
|
audioMuted,
|
||||||
|
audioVolume,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className={classnames(
|
||||||
|
"embbededMediaPlayerWrapper",
|
||||||
|
{
|
||||||
|
["hovering"]: this.state.showControls,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onMouseEnter={this.onMouse}
|
||||||
|
onMouseLeave={this.onMouse}
|
||||||
|
>
|
||||||
|
<div className="player">
|
||||||
|
<div className="minimize_btn">
|
||||||
|
<antd.Button
|
||||||
|
icon={<Icons.MdFirstPage />}
|
||||||
|
onClick={this.minimize}
|
||||||
|
shape="circle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
{
|
||||||
|
currentPlaying?.artist && <div className="artist">
|
||||||
|
<h3>
|
||||||
|
{currentPlaying?.artist ?? "Unknown"}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="indicators">
|
||||||
{
|
{
|
||||||
!loading && <div className="artist">
|
loading && <antd.Spin />
|
||||||
<h3>
|
|
||||||
{currentPlaying?.artist ?? "Unknown"}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="indicators">
|
|
||||||
{loading ?
|
|
||||||
<antd.Spin /> :
|
|
||||||
<>
|
|
||||||
<Icons.MdOutlineExplicit />
|
|
||||||
<Icons.MdOutlineHighQuality />
|
|
||||||
</>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="controls">
|
<div className="controls">
|
||||||
<div>
|
<AudioPlayerChangeModeButton />
|
||||||
<antd.Button
|
<antd.Button
|
||||||
type="ghost"
|
type="ghost"
|
||||||
shape="round"
|
shape="round"
|
||||||
onClick={onClickPreviousButton}
|
|
||||||
icon={<Icons.ChevronLeft />}
|
icon={<Icons.ChevronLeft />}
|
||||||
|
onClick={this.onClickPreviousButton}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="playButton">
|
|
||||||
<antd.Button
|
<antd.Button
|
||||||
type="primary"
|
type="primary"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
icon={playing ? <Icons.Pause /> : <Icons.Play />}
|
icon={playbackStatus === "playing" ? <Icons.Pause /> : <Icons.Play />}
|
||||||
onClick={onClickPlayButton}
|
onClick={this.onClickPlayButton}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<antd.Button
|
<antd.Button
|
||||||
type="ghost"
|
type="ghost"
|
||||||
shape="round"
|
shape="round"
|
||||||
onClick={onClickNextButton}
|
|
||||||
icon={<Icons.ChevronRight />}
|
icon={<Icons.ChevronRight />}
|
||||||
|
onClick={this.onClickNextButton}
|
||||||
/>
|
/>
|
||||||
</div>
|
<antd.Popover
|
||||||
<antd.Popover content={React.createElement(AudioVolume, { onChange: updateVolume, defaultValue: volume })} trigger="hover">
|
content={React.createElement(
|
||||||
<div
|
AudioVolume,
|
||||||
onClick={toogleMute}
|
{ onChange: this.updateVolume, defaultValue: audioVolume }
|
||||||
className="muteButton"
|
)}
|
||||||
|
trigger="hover"
|
||||||
>
|
>
|
||||||
{mute ? <Icons.VolumeX /> : <Icons.Volume2 />}
|
<div
|
||||||
</div>
|
className="muteButton"
|
||||||
</antd.Popover>
|
onClick={this.toogleMute}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
audioMuted
|
||||||
|
? <Icons.VolumeX />
|
||||||
|
: <Icons.Volume2 />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</antd.Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SeekBar
|
||||||
|
stopped={playbackStatus === "stopped"}
|
||||||
|
playing={playbackStatus === "playing"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
<PlayerStatus />
|
|
||||||
</div>
|
|
||||||
}
|
}
|
@ -5,58 +5,162 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
||||||
margin: 50px;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
.cover {
|
align-items: center;
|
||||||
position: relative;
|
justify-content: center;
|
||||||
z-index: 310;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 270px;
|
||||||
height: 200px;
|
height: fit-content;
|
||||||
background-position: center;
|
|
||||||
background-size: cover;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
border-radius: 10px;
|
|
||||||
|
|
||||||
transition: all 0.3s ease-in-out;
|
margin: 40px;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
&.hovering {
|
||||||
|
.actions_wrapper {
|
||||||
|
.actions {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-90%, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimize_btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions_wrapper {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
z-index: 300;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
height: 96%;
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 60px;
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
padding: 10px 8px 10px 5px;
|
||||||
|
|
||||||
|
background-color: rgba(var(--bg_color_2), 0.3);
|
||||||
|
|
||||||
|
transform: translate(-25%, 0);
|
||||||
|
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimize_btn {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
z-index: 330;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
transform: translate(60%, 60%);
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.player {
|
.player {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 320;
|
z-index: 310;
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
min-width: 250px;
|
align-items: flex-start;
|
||||||
max-width: 250px;
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
padding-bottom: 40px;
|
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
background-color: var(--background-color-accent);
|
background-color: var(--background-color-accent);
|
||||||
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
// animate in from bottom to top
|
// animate in from bottom to top
|
||||||
animation: fadeIn 150ms ease-out forwards;
|
animation: fadeIn 150ms ease-out forwards;
|
||||||
|
|
||||||
transition: all 150ms ease-out;
|
transition: all 150ms ease-out;
|
||||||
|
|
||||||
transform: translate(0, 20px);
|
|
||||||
|
|
||||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
|
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
z-index: 320;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 250px;
|
||||||
|
min-height: 250px;
|
||||||
|
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
width: 100%;
|
margin: 15px 0;
|
||||||
|
|
||||||
margin-bottom: 15px;
|
width: 100%;
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -115,7 +219,7 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-evenly;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@ -124,9 +228,9 @@
|
|||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
justify-content: space-evenly;
|
|
||||||
|
|
||||||
.muteButton {
|
.muteButton {
|
||||||
|
padding: 10px;
|
||||||
svg {
|
svg {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
@ -135,24 +239,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
position: relative;
|
|
||||||
z-index: 330;
|
z-index: 330;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
width: 100%;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
align-self: center;
|
||||||
|
|
||||||
|
width: 90%;
|
||||||
|
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
padding: 10px;
|
margin: 20px 0 10px 0;
|
||||||
|
|
||||||
background-color: var(--background-color-accent);
|
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
// create box shadow only top of the player
|
|
||||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
|
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -178,6 +282,47 @@
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bpmTicker {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
background-color: var(--background-color-contrast);
|
||||||
|
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
&.pulse {
|
||||||
|
animation: pulseBPM;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
|
||||||
|
animation-duration: 1s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulseBPM {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-popover-inner-content {
|
.ant-popover-inner-content {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user