improve handlers & style

This commit is contained in:
SrGooglo 2023-05-29 16:05:14 +00:00
parent 5d6629402d
commit 95bae7cb61
2 changed files with 314 additions and 145 deletions

View File

@ -1,10 +1,8 @@
import React from "react" import React from "react"
import classnames from "classnames" import classnames from "classnames"
import { Button } from "antd" import Marquee from "react-fast-marquee"
import UseAnimations from "react-useanimations" import Controls from "components/Player/Controls"
import LoadingAnimation from "react-useanimations/lib/loading"
import { Icons } from "components/Icons"
import Image from "components/Image" import Image from "components/Image"
@ -12,6 +10,16 @@ import request from "comty.js/handlers/request"
import "./index.less" import "./index.less"
function RGBStringToValues(rgbString) {
if (!rgbString) {
return [0, 0, 0]
}
const rgb = rgbString.replace("rgb(", "").replace(")", "").split(",").map((v) => parseInt(v))
return [rgb[0], rgb[1], rgb[2]]
}
function composeRgbValues(values) { function composeRgbValues(values) {
let value = "" let value = ""
@ -37,12 +45,33 @@ function calculateLineTime(line) {
return line.endTimeMs - line.startTimeMs return line.endTimeMs - line.startTimeMs
} }
function isOverflown(element) {
if (!element) {
console.log("element is null")
return false
}
return element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
}
class PlayerController extends React.Component { class PlayerController extends React.Component {
state = { state = {
hovering: false,
colorAnalysis: null, colorAnalysis: null,
currentState: null,
currentDragWidth: 0, currentDragWidth: 0,
titleOverflown: false,
currentDuration: 0,
currentTime: 0,
currentPlaying: app.cores.player.getState("currentAudioManifest"),
loading: app.cores.player.getState("loading") ?? false,
playbackStatus: app.cores.player.getState("playbackStatus") ?? "stopped",
audioMuted: app.cores.player.getState("audioMuted") ?? false,
volume: app.cores.player.getState("audioVolume"),
syncModeLocked: app.cores.player.getState("syncModeLocked"),
syncMode: app.cores.player.getState("syncMode"),
} }
events = { events = {
@ -50,16 +79,37 @@ class PlayerController extends React.Component {
this.setState({ colorAnalysis }) this.setState({ colorAnalysis })
}, },
"player.seek.update": (seekTime) => { "player.seek.update": (seekTime) => {
const updatedState = this.state.currentState
updatedState.time = seekTime
this.setState({ this.setState({
currentState: updatedState, currentTime: seekTime,
}) })
}, },
"player.status.update": (data) => {
this.setState({ playbackStatus: data })
},
"player.current.update": (data) => {
this.setState({ titleOverflown: false })
this.setState({ currentPlaying: data })
},
"player.syncModeLocked.update": (to) => {
this.setState({ syncModeLocked: to })
},
"player.syncMode.update": (to) => {
this.setState({ syncMode: to })
},
"player.mute.update": (data) => {
this.setState({ audioMuted: data })
},
"player.volume.update": (data) => {
this.setState({ audioVolume: data })
},
"player.loading.update": (data) => {
this.setState({ loading: data })
},
} }
titleRef = React.createRef()
startSync() { startSync() {
// create a interval to get state from player // create a interval to get state from player
if (this.syncInterval) { if (this.syncInterval) {
@ -69,7 +119,15 @@ class PlayerController extends React.Component {
this.syncInterval = setInterval(() => { this.syncInterval = setInterval(() => {
const currentState = app.cores.player.currentState() const currentState = app.cores.player.currentState()
this.setState({ currentState }) this.setState({
currentDuration: currentState.duration,
currentTime: currentState.time,
colorAnalysis: currentState.colorAnalysis,
})
const titleOverflown = isOverflown(this.titleRef.current)
this.setState({ titleOverflown: titleOverflown })
}, 800) }, 800)
} }
@ -82,13 +140,21 @@ class PlayerController extends React.Component {
} }
onClickTooglePlayButton = () => { onClickTooglePlayButton = () => {
if (this.state.currentState?.playbackStatus === "playing") { if (this.state?.playbackStatus === "playing") {
app.cores.player.playback.pause() app.cores.player.playback.pause()
} else { } else {
app.cores.player.playback.play() app.cores.player.playback.play()
} }
} }
updateVolume = (value) => {
app.cores.player.volume(value)
}
toogleMute = () => {
app.cores.player.toogleMute()
}
componentDidMount() { componentDidMount() {
for (const event in this.events) { for (const event in this.events) {
app.eventBus.on(event, this.events[event]) app.eventBus.on(event, this.events[event])
@ -99,6 +165,26 @@ class PlayerController extends React.Component {
} }
this.startSync() this.startSync()
// // create a intersection observer to check if title is overflown
// // if the entire title is not visible, we will use marquee
// this.titleObserver = new IntersectionObserver((entries) => {
// for (const entry of entries) {
// if (entry.isIntersecting) {
// this.setState({ titleOverflown: false })
// } else {
// this.setState({ titleOverflown: true })
// }
// }
// }, {
// root: null,
// rootMargin: "0px",
// threshold: 1.0,
// })
console.log(this.titleRef.current)
//this.titleObserver.observe(this.titleRef.current)
} }
componentWillUnmount() { componentWillUnmount() {
@ -121,70 +207,91 @@ class PlayerController extends React.Component {
} }
render() { render() {
//const bgColor = RGBStringToValues(getComputedStyle(document.documentElement).getPropertyValue("--background-color-accent-values"))
return <div className="player_controller_wrapper"> return <div className="player_controller_wrapper">
<div <div
onMouseEnter={() => {
this.setState({ hovering: true })
}}
onMouseLeave={() => {
this.setState({ hovering: false })
}}
className={classnames( className={classnames(
"player_controller", "player_controller",
{
["player_controller--hovering"]: this.state.hovering || this.state.dragging,
}
)} )}
> >
<div className="player_controller_cover"> <div className="player_controller_cover">
<Image <Image
src={this.state.currentState?.manifest?.thumbnail} src={this.state.currentPlaying?.thumbnail ?? "/assets/no_song.png"}
/> />
</div> </div>
<div className="player_controller_left"> <div className="player_controller_left">
<div className="player_controller_info"> <div className="player_controller_info">
<div className="player_controller_info_title"> <div className="player_controller_info_title">
{this.state.currentState?.manifest?.title} {
<h4
ref={this.titleRef}
className={classnames(
"player_controller_info_title_text",
{
["overflown"]: this.state.titleOverflown,
}
)}
>
{
this.state.plabackState === "stopped" ? "Nothing is playing" : <>
{this.state.currentPlaying?.title ?? "Nothing is playing"}
</>
}
</h4>
}
{this.state.titleOverflown &&
<Marquee
//gradient
//gradientColor={bgColor}
//gradientWidth={20}
play={this.state.plabackState !== "stopped"}
>
<h4>
{
this.state.plabackState === "stopped" ? "Nothing is playing" : <>
{this.state.currentPlaying?.title ?? "Nothing is playing"}
</>
}
</h4>
</Marquee>}
</div> </div>
<div className="player_controller_info_artist"> <div className="player_controller_info_artist">
{this.state.currentState?.manifest?.artist} - {this.state.currentState?.manifest?.album} {
this.state.currentPlaying?.artist && <>
<h3>
{this.state.currentPlaying?.artist ?? "Unknown"}
</h3>
{
this.state.currentPlaying?.album && <>
<span> - </span>
<h3>
{this.state.currentPlaying?.album ?? "Unknown"}
</h3>
</>
}
</>
}
</div> </div>
</div> </div>
<div className="player_controller_controls"> <Controls
<Button className="player_controller_controls"
type="ghost" controls={{
shape="round" previous: this.onClickPreviousButton,
icon={<Icons.ChevronLeft />} toogle: this.onClickTooglePlayButton,
onClick={this.onClickPreviousButton} next: this.onClickNextButton,
disabled={this.state.currentState?.syncModeLocked} }}
/> syncModeLocked={this.state.syncModeLocked}
<Button playbackStatus={this.state.playbackStatus}
className="playButton" loading={this.state.loading}
type="primary" audioVolume={this.state.audioVolume}
shape="circle" audioMuted={this.state.audioMuted}
icon={this.state.currentState?.playbackStatus === "playing" ? <Icons.MdPause /> : <Icons.MdPlayArrow />} onVolumeUpdate={this.updateVolume}
onClick={this.onClickTooglePlayButton} onMuteUpdate={this.toogleMute}
disabled={this.state.currentState?.syncModeLocked} />
>
{
this.state.currentState?.loading && <div className="loadCircle">
<UseAnimations
animation={LoadingAnimation}
size="100%"
/>
</div>
}
</Button>
<Button
type="ghost"
shape="round"
icon={<Icons.ChevronRight />}
onClick={this.onClickNextButton}
disabled={this.state.currentState?.syncModeLocked}
/>
</div>
</div> </div>
<div className="player_controller_progress_wrapper"> <div className="player_controller_progress_wrapper">
@ -197,7 +304,7 @@ class PlayerController extends React.Component {
}} }}
onMouseUp={(e) => { onMouseUp={(e) => {
const rect = e.currentTarget.getBoundingClientRect() const rect = e.currentTarget.getBoundingClientRect()
const seekTime = this.state.currentState?.duration * (e.clientX - rect.left) / rect.width const seekTime = this.state.currentDuration * (e.clientX - rect.left) / rect.width
this.onDragEnd(seekTime) this.onDragEnd(seekTime)
}} }}
@ -210,7 +317,7 @@ class PlayerController extends React.Component {
> >
<div className="player_controller_progress_bar" <div className="player_controller_progress_bar"
style={{ style={{
width: `${this.state.dragging ? this.state.currentDragWidth : this.state.currentState?.time / this.state.currentState?.duration * 100}%` width: `${this.state.dragging ? this.state.currentDragWidth : this.state.currentTime / this.state.currentDuration * 100}%`
}} }}
/> />
</div> </div>
@ -234,20 +341,11 @@ export default class SyncLyrics extends React.Component {
colorAnalysis: null, colorAnalysis: null,
classnames: [ classnames: {
{ "cinematic-mode": false,
name: "cinematic-mode", "centered-player": false,
enabled: false, "video-canvas-enabled": false,
}, }
{
name: "centered-player",
enabled: false,
},
{
name: "video-canvas-enabled",
enabled: false,
}
]
} }
visualizerRef = React.createRef() visualizerRef = React.createRef()
@ -274,6 +372,56 @@ export default class SyncLyrics extends React.Component {
} }
} }
toogleClassName = (className, to) => {
if (typeof to === "undefined") {
to = !this.state.classnames[className]
}
if (to) {
if (this.state.classnames[className] === true) {
return false
}
//app.message.info("Toogling on " + className)
this.setState({
classnames: {
...this.state.classnames,
[className]: true
},
})
return true
} else {
if (this.state.classnames[className] === false) {
return false
}
//app.message.info("Toogling off " + className)
this.setState({
classnames: {
...this.state.classnames,
[className]: false
},
})
return true
}
}
toogleVideoCanvas = (to) => {
return this.toogleClassName("video-canvas-enabled", to)
}
toogleCenteredControllerMode = (to) => {
return this.toogleClassName("centered-player", to)
}
toogleCinematicMode = (to) => {
return this.toogleClassName("cinematic-mode", to)
}
isCurrentLine = (line) => { isCurrentLine = (line) => {
if (!this.state.currentLine) { if (!this.state.currentLine) {
return false return false
@ -342,9 +490,13 @@ export default class SyncLyrics extends React.Component {
if (data.canvas_url) { if (data.canvas_url) {
//app.message.info("Video canvas loaded") //app.message.info("Video canvas loaded")
console.log(`[SyncLyrics] Video canvas loaded`)
this.toogleVideoCanvas(true) this.toogleVideoCanvas(true)
} else { } else {
//app.message.info("No video canvas available for this song") //app.message.info("No video canvas available for this song")
console.log(`[SyncLyrics] No video canvas available for this song`)
this.toogleVideoCanvas(false) this.toogleVideoCanvas(false)
} }
@ -352,10 +504,13 @@ export default class SyncLyrics extends React.Component {
if (data.lines.length === 0 || data.syncType !== "LINE_SYNCED") { if (data.lines.length === 0 || data.syncType !== "LINE_SYNCED") {
//app.message.info("No lyrics available for this song") //app.message.info("No lyrics available for this song")
console.log(`[SyncLyrics] No lyrics available for this song, sync type [${data.syncType}]`)
this.toogleCinematicMode(false) this.toogleCinematicMode(false)
this.toogleCenteredControllerMode(true) this.toogleCenteredControllerMode(true)
} else { } else {
//app.message.info("Lyrics loaded, starting sync...") //app.message.info("Lyrics loaded, starting sync...")
console.log(`[SyncLyrics] Starting sync with type [${data.syncType}]`)
this.toogleCenteredControllerMode(false) this.toogleCenteredControllerMode(false)
this.startLyricsSync() this.startLyricsSync()
@ -365,65 +520,11 @@ export default class SyncLyrics extends React.Component {
this.setState({ this.setState({
loading: false, loading: false,
syncType: data.syncType, syncType: data.syncType,
canvas_url: data.canvas_url, canvas_url: data.canvas_url ?? null,
lyrics: data.lines, lyrics: data.lines,
}) })
} }
toogleClassName = (className, to) => {
let currentState = this.state.classnames.find((c) => c.name === className)
if (!currentState) {
return false
}
if (typeof to === "undefined") {
to = !currentState?.enabled
}
if (to) {
if (currentState.enabled) {
return false
}
//app.message.info("Toogling on " + className)
currentState.enabled = true
this.setState({
classnames: this.state.classnames,
})
return true
} else {
if (!currentState.enabled) {
return false
}
//app.message.info("Toogling off " + className)
currentState.enabled = false
this.setState({
classnames: this.state.classnames,
})
return true
}
}
toogleVideoCanvas = (to) => {
return this.toogleClassName("video-canvas-enabled", to)
}
toogleCenteredControllerMode = (to) => {
return this.toogleClassName("centered-player", to)
}
toogleCinematicMode = (to) => {
return this.toogleClassName("cinematic-mode", to)
}
startLyricsSync = () => { startLyricsSync = () => {
// create interval to sync lyrics // create interval to sync lyrics
if (this.syncInterval) { if (this.syncInterval) {
@ -495,8 +596,16 @@ export default class SyncLyrics extends React.Component {
if (this.state.canvas_url) { if (this.state.canvas_url) {
if (line.words === "♪" || line.words === "♫" || line.words === " " || line.words === "") { if (line.words === "♪" || line.words === "♫" || line.words === " " || line.words === "") {
//console.log(`[SyncLyrics] Toogling cinematic mode on because line is empty`)
this.toogleCinematicMode(true) this.toogleCinematicMode(true)
} else { } else {
//console.log(`[SyncLyrics] Toogling cinematic mode off because line is not empty`)
this.toogleCinematicMode(false)
}
} else {
if (this.state.classnames["cinematic-mode"] === true) {
this.toogleCinematicMode(false) this.toogleCinematicMode(false)
} }
} }
@ -587,14 +696,10 @@ export default class SyncLyrics extends React.Component {
"lyrics_viewer", "lyrics_viewer",
{ {
["text_dark"]: this.state.colorAnalysis?.isDark ?? false, ["text_dark"]: this.state.colorAnalysis?.isDark ?? false,
...this.state.classnames.map((classname) => { ...Object.entries(this.state.classnames).reduce((acc, [key, value]) => {
return { return {
[classname.name]: classname.enabled, ...acc,
} [key]: value,
}).reduce((a, b) => {
return {
...a,
...b,
} }
}, {}), }, {}),
}, },
@ -628,7 +733,7 @@ export default class SyncLyrics extends React.Component {
className="lyrics_viewer_thumbnail" className="lyrics_viewer_thumbnail"
> >
<Image <Image
src={this.state.currentManifest?.thumbnail} src={this.state.currentManifest?.thumbnail ?? "/assets/no_song.png"}
ref={this.thumbnailRef} ref={this.thumbnailRef}
/> />
</div> </div>

View File

@ -1,4 +1,7 @@
@enabled-video-canvas-opacity: 0.4; @enabled-video-canvas-opacity: 0.4;
// in px
@cover-width: 150px;
@left-panel-width: 300px;
.lyrics_viewer { .lyrics_viewer {
display: flex; display: flex;
@ -66,10 +69,24 @@
border-radius: 18px; border-radius: 18px;
gap: 50px; gap: 0;
padding: 20px 40px;
.player_controller_left {
width: 100%;
max-width: 100%;
min-width: 100%;
}
.player_controller_cover { .player_controller_cover {
width: 0px; width: 0px;
min-width: 0px;
img {
min-width: 0px;
}
} }
.player_controller_info { .player_controller_info {
@ -87,7 +104,7 @@
-webkit-backdrop-filter: blur(0px) -webkit-backdrop-filter: blur(0px)
} }
.lyrics_viewer_background { .lyrics_viewer_video_canvas {
video { video {
opacity: 1; opacity: 1;
} }
@ -264,13 +281,18 @@
transition: all 150ms ease-in-out; transition: all 150ms ease-in-out;
.marquee-container {
gap: 60px;
}
.player_controller { .player_controller {
box-sizing: border-box;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
width: 25vw; width: 100%;
min-width: 350px; min-width: 350px;
max-width: 500px; max-width: 500px;
@ -299,7 +321,7 @@
overflow: hidden; overflow: hidden;
&.player_controller--hovering { &:hover {
.player_controller_controls { .player_controller_controls {
height: 8vh; height: 8vh;
max-height: 100px; max-height: 100px;
@ -326,7 +348,10 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 10vw; width: @cover-width;
min-width: @cover-width;
max-width: @cover-width;
height: 100%; height: 100%;
img { img {
@ -340,6 +365,7 @@
} }
.player_controller_left { .player_controller_left {
flex: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -347,6 +373,7 @@
justify-content: center; justify-content: center;
height: 100%; height: 100%;
width: @left-panel-width;
transition: all 150ms ease-in-out; transition: all 150ms ease-in-out;
@ -354,7 +381,9 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; //align-items: flex-start;
width: 100%;
gap: 10px; gap: 10px;
@ -364,12 +393,45 @@
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 600; font-weight: 600;
color: var(--background-color-contrast) width: 100%;
color: var(--background-color-contrast);
h4 {
margin: 0;
}
.player_controller_info_title_text {
transition: all 150ms ease-in-out;
width: 90%;
overflow: hidden;
// do not wrap text
white-space: nowrap;
&.overflown {
opacity: 0;
height: 0px;
}
}
} }
.player_controller_info_artist { .player_controller_info_artist {
font-size: 0.8rem; display: flex;
flex-direction: row;
align-items: center;
gap: 7px;
font-size: 0.6rem;
font-weight: 400; font-weight: 400;
h3 {
margin: 0;
}
} }
} }
} }
@ -385,6 +447,8 @@
padding: 10px; padding: 10px;
width: 100%;
height: 0px; height: 0px;
opacity: 0; opacity: 0;