diff --git a/packages/app/src/components/EmbbededMediaPlayer/index.jsx b/packages/app/src/components/EmbbededMediaPlayer/index.jsx index a9732948..43df5f9e 100755 --- a/packages/app/src/components/EmbbededMediaPlayer/index.jsx +++ b/packages/app/src/components/EmbbededMediaPlayer/index.jsx @@ -1,12 +1,12 @@ import React from "react" 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" -// 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: Add close button // TODO: Add repeat & shuffle mode @@ -31,14 +31,33 @@ const AudioVolume = (props) => { } -const PlayerStatus = React.memo((props) => { - const [playing, setPlaying] = React.useState(false) - const [time, setTime] = React.useState("00:00") - const [duration, setDuration] = React.useState("00:00") - const [progressBar, setProgressBar] = React.useState(0) +class SeekBar extends React.Component { + state = { + timeText: "00:00", + durationText: "00:00", + sliderTime: 0, + sliderLock: false, + } - const updateDuration = () => { - const audioDuration = app.AudioPlayer.currentAudio.instance.duration() + 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() + + console.log(`Audio duration: ${audioDuration}`) // convert duration to minutes and seconds const minutes = Math.floor(audioDuration / 60) @@ -53,12 +72,14 @@ const PlayerStatus = React.memo((props) => { const secondsString = seconds < 10 ? `0${seconds}` : seconds // set duration - setDuration(`${minutesString}:${secondsString}`) + this.setState({ + durationText: `${minutesString}:${secondsString}` + }) } - const updateTimer = () => { - // get audio seek - const seek = app.AudioPlayer.currentAudio.instance.seek() + calculateTime = () => { + // get current audio seek + const seek = app.cores.player.seek() // convert seek to minutes and seconds const minutes = Math.floor(seek / 60) @@ -73,272 +94,360 @@ const PlayerStatus = React.memo((props) => { const secondsString = seconds < 10 ? `0${seconds}` : seconds // set time - setTime(`${minutesString}:${secondsString}`) + this.setState({ + timeText: `${minutesString}:${secondsString}` + }) } - const updateProgressBar = () => { - const seek = app.AudioPlayer.currentAudio.instance.seek() - const duration = app.AudioPlayer.currentAudio.instance.duration() + updateProgressBar = () => { + if (this.state.sliderLock) { + return + } + + const seek = app.cores.player.seek() + const duration = app.cores.player.duration() const percent = (seek / duration) * 100 - setProgressBar(percent) + this.setState({ + sliderTime: percent + }) } - const onUpdateSeek = (value) => { - // calculate the duration of the audio - const duration = app.AudioPlayer.currentAudio.instance.duration() - - // 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) + updateAll = () => { + this.calculateTime() + this.updateProgressBar() } - const tooltipFormatter = (value) => { - if (!app.AudioPlayer.currentAudio) { - return "00:00" - } + events = { + "player.status.update": (status) => { + 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 - const minutes = Math.floor(seek / 60) - - // 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() + break + default: + break } - }, 1000) + }, + "player.current.update": (currentAudioManifest) => { + console.log(`Player current audio updated:`, currentAudioManifest) - return () => { - clearInterval(interval) - } - }, [playing]) + this.updateAll() - const events = { - "audioPlayer.seeked": () => { - updateTimer() - updateProgressBar() + this.setState({ + timeText: "00:00", + sliderTime: 0, + }) + + this.calculateDuration() }, - "audioPlayer.playing": () => { - updateDuration() - updateTimer() - updateProgressBar() + "player.duration.update": (duration) => { + console.log(`Player duration updated: ${duration}`) + + this.calculateDuration() }, - "audioPlayer.paused": () => { - setPlaying(false) - }, - "audioPlayer.stopped": () => { - setPlaying(false) + "player.seek.update": (seek) => { + console.log(`Player seek updated: ${seek}`) + + this.calculateTime() + this.updateAll() } } - React.useEffect(() => { - // listen events - for (const [event, callback] of Object.entries(events)) { + tick = () => { + if (this.props.playing) { + 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) } + } - return () => { - // remove events - for (const [event, callback] of Object.entries(events)) { - app.eventBus.off(event, callback) - } + componentWillUnmount = () => { + for (const [event, callback] of Object.entries(this.events)) { + app.eventBus.off(event, callback) } - }, []) + } - return
-
- -
-
-
- {time} + componentDidUpdate = (prevProps, prevState) => { + if (this.props.playing !== prevProps.playing) { + this.tick() + } + } + + render() { + return
+
+ { + 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() + } + }} + />
-
- {duration} +
+
+ {this.state.timeText} +
+
+ {this.state.durationText} +
-
-}) + } +} -export default (props) => { - const [mute, setMute] = React.useState(app.AudioPlayer.audioMuted) - const [volume, setVolume] = React.useState(app.AudioPlayer.audioVolume) +const AudioPlayerChangeModeButton = (props) => { + const [mode, setMode] = React.useState(app.cores.player.playback.mode()) - const [currentPlaying, setCurrentPlaying] = React.useState(null) - const [playing, setPlaying] = React.useState(false) - const [loading, setLoading] = React.useState(true) - - const toogleMute = () => { - setMute(app.AudioPlayer.toogleMute()) + const modeToIcon = { + "normal": "MdArrowForward", + "repeat": "MdRepeat", + "shuffle": "MdShuffle", } - const updateVolume = (value) => { - console.log("Updating volume", value) + const onClick = () => { + const modes = Object.keys(modeToIcon) - setVolume(app.AudioPlayer.setVolume(value)) + const newMode = modes[(modes.indexOf(mode) + 1) % modes.length] - if (value > 0) { - setMute(false) - } + app.cores.player.playback.mode(newMode) + + setMode(newMode) } - const onClickPlayButton = () => { - setPlaying(!playing) + return +} - if (playing) { - app.AudioPlayer.pauseAudioQueue() - } else { - app.AudioPlayer.playCurrentAudio() - } +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, } - const onClickNextButton = () => { - app.AudioPlayer.nextAudio() - } - - const onClickPreviousButton = () => { - app.AudioPlayer.previousAudio() - } - - const busEvents = { - "audioPlayer.playing": (data) => { - if (data) { - setCurrentPlaying(data) - } - - setLoading(false) - setPlaying(true) + events = { + "player.bpm.update": (data) => { + this.setState({ bpm: data }) }, - "audioPlayer.pause": () => { - setPlaying(false) + "player.loading.update": (data) => { + this.setState({ loading: data }) }, - "audioPlayer.stopped": () => { - setPlaying(false) + "player.status.update": (data) => { + this.setState({ playbackStatus: data }) }, - "audioPlayer.loaded": () => { - setLoading(false) + "player.current.update": (data) => { + this.setState({ currentPlaying: data }) }, - "audioPlayer.loading": () => { - setLoading(true) + "player.mute.update": (data) => { + 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 - React.useEffect(() => { - for (const event in busEvents) { - app.eventBus.on(event, busEvents[event]) - } + minimize = () => { - return () => { - for (const event in busEvents) { - app.eventBus.off(event, busEvents[event]) - } - } - }, []) + } - console.log(currentPlaying) + updateVolume = (value) => { + app.cores.player.volume(value) + } - return
-
+ toogleMute = (to) => { + app.cores.player.toogleMute(to) + } -
-
-
-
-

- {loading ? "Loading..." : (currentPlaying?.title ?? "Untitled")} -

+ onClickPlayButton = () => { + app.cores.player.playback.toogle() + } + + onClickPreviousButton = () => { + app.cores.player.playback.previous() + } + + onClickNextButton = () => { + app.cores.player.playback.next() + } + + render() { + const { + loading, + currentPlaying, + playbackStatus, + audioMuted, + audioVolume, + } = this.state + + return
+
+
+ } + onClick={this.minimize} + shape="circle" + /> +
+
+
+
+
+

+ { + currentPlaying?.title + ? currentPlaying?.title + : (loading ? "Loading..." : (currentPlaying?.title ?? "Untitled")) + } +

+
+
+ { + currentPlaying?.artist &&
+

+ {currentPlaying?.artist ?? "Unknown"} +

+
+ } +
-
+
{ - !loading &&
-

- {currentPlaying?.artist ?? "Unknown"} -

-
+ loading && }
-
- {loading ? - : - <> - - - } -
-
-
-
+
+ } + onClick={this.onClickPreviousButton} /> -
-
: } - onClick={onClickPlayButton} + icon={playbackStatus === "playing" ? : } + onClick={this.onClickPlayButton} /> -
-
} + onClick={this.onClickNextButton} /> -
- -
- {mute ? : } -
-
+
+ { + audioMuted + ? + : + } +
+ +
+ +
- - -
+ } } \ No newline at end of file diff --git a/packages/app/src/components/EmbbededMediaPlayer/index.less b/packages/app/src/components/EmbbededMediaPlayer/index.less index 930813a5..8055dba9 100755 --- a/packages/app/src/components/EmbbededMediaPlayer/index.less +++ b/packages/app/src/components/EmbbededMediaPlayer/index.less @@ -5,58 +5,162 @@ right: 0; bottom: 0; - margin: 50px; + display: flex; + flex-direction: row; - .cover { - position: relative; - z-index: 310; + align-items: center; + justify-content: center; - width: 100%; - height: 200px; - background-position: center; - background-size: cover; - background-repeat: no-repeat; - border-radius: 10px; + width: 270px; + height: fit-content; - 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 { position: relative; - z-index: 320; + z-index: 310; display: inline-flex; flex-direction: column; - min-width: 250px; - max-width: 250px; + align-items: flex-start; + justify-content: center; + + width: 100%; + height: 100%; padding: 10px; - padding-bottom: 40px; border-radius: 8px; background-color: var(--background-color-accent); - align-items: flex-start; - justify-content: center; - // animate in from bottom to top animation: fadeIn 150ms ease-out forwards; transition: all 150ms ease-out; - transform: translate(0, 20px); - 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 { display: flex; flex-direction: row; - width: 100%; + margin: 15px 0; - margin-bottom: 15px; + width: 100%; .info { display: flex; @@ -115,7 +219,7 @@ display: inline-flex; align-items: center; - justify-content: center; + justify-content: space-evenly; width: 100%; @@ -124,9 +228,9 @@ margin: 0 !important; } - justify-content: space-evenly; .muteButton { + padding: 10px; svg { font-size: 1rem; } @@ -135,24 +239,24 @@ } .status { - position: relative; z-index: 330; display: flex; flex-direction: column; - width: 100%; + align-items: center; + justify-content: center; + + align-self: center; + + width: 90%; + height: fit-content; - padding: 10px; - - background-color: var(--background-color-accent); + margin: 20px 0 10px 0; border-radius: 8px; - // create box shadow only top of the player - box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2); - .progress { width: 100%; } @@ -178,6 +282,47 @@ 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 {