mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-11 03:24:16 +00:00
added a lyrics viewer
This commit is contained in:
parent
97a5bc449f
commit
791a42c020
647
packages/app/src/pages/lyrics/index.jsx
Normal file
647
packages/app/src/pages/lyrics/index.jsx
Normal file
@ -0,0 +1,647 @@
|
|||||||
|
import React from "react"
|
||||||
|
import classnames from "classnames"
|
||||||
|
import { Button } from "antd"
|
||||||
|
|
||||||
|
import UseAnimations from "react-useanimations"
|
||||||
|
import LoadingAnimation from "react-useanimations/lib/loading"
|
||||||
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
|
import Image from "components/Image"
|
||||||
|
|
||||||
|
import request from "comty.js/handlers/request"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
function composeRgbValues(values) {
|
||||||
|
let value = ""
|
||||||
|
|
||||||
|
// only get the first 3 values
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
// if last value, don't add comma
|
||||||
|
if (i === 2) {
|
||||||
|
value += `${values[i]}`
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value += `${values[i]}, `
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateLineTime(line) {
|
||||||
|
if (!line) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return line.endTimeMs - line.startTimeMs
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayerController extends React.Component {
|
||||||
|
state = {
|
||||||
|
hovering: false,
|
||||||
|
colorAnalysis: null,
|
||||||
|
currentState: null,
|
||||||
|
currentDragWidth: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
events = {
|
||||||
|
"player.coverColorAnalysis.update": (colorAnalysis) => {
|
||||||
|
this.setState({ colorAnalysis })
|
||||||
|
},
|
||||||
|
"player.seek.update": (seekTime) => {
|
||||||
|
const updatedState = this.state.currentState
|
||||||
|
|
||||||
|
updatedState.time = seekTime
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
currentState: updatedState,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
startSync() {
|
||||||
|
// create a interval to get state from player
|
||||||
|
if (this.syncInterval) {
|
||||||
|
clearInterval(this.syncInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.syncInterval = setInterval(() => {
|
||||||
|
const currentState = app.cores.player.currentState()
|
||||||
|
|
||||||
|
this.setState({ currentState })
|
||||||
|
}, 800)
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickPreviousButton = () => {
|
||||||
|
app.cores.player.playback.previous()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickNextButton = () => {
|
||||||
|
app.cores.player.playback.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickTooglePlayButton = () => {
|
||||||
|
if (this.state.currentState?.playbackStatus === "playing") {
|
||||||
|
app.cores.player.playback.pause()
|
||||||
|
} else {
|
||||||
|
app.cores.player.playback.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
for (const event in this.events) {
|
||||||
|
app.eventBus.on(event, this.events[event])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.syncInterval) {
|
||||||
|
clearInterval(this.syncInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startSync()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
for (const event in this.events) {
|
||||||
|
app.eventBus.off(event, this.events[event])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.syncInterval) {
|
||||||
|
clearInterval(this.syncInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragEnd = (seekTime) => {
|
||||||
|
this.setState({
|
||||||
|
currentDragWidth: 0,
|
||||||
|
dragging: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
app.cores.player.seek(seekTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div className="player_controller_wrapper">
|
||||||
|
<div
|
||||||
|
onMouseEnter={() => {
|
||||||
|
this.setState({ hovering: true })
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
this.setState({ hovering: false })
|
||||||
|
}}
|
||||||
|
className={classnames(
|
||||||
|
"player_controller",
|
||||||
|
{
|
||||||
|
["player_controller--hovering"]: this.state.hovering || this.state.dragging,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="player_controller_cover">
|
||||||
|
<Image
|
||||||
|
src={this.state.currentState?.manifest?.thumbnail}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="player_controller_left">
|
||||||
|
<div className="player_controller_info">
|
||||||
|
<div className="player_controller_info_title">
|
||||||
|
{this.state.currentState?.manifest?.title}
|
||||||
|
</div>
|
||||||
|
<div className="player_controller_info_artist">
|
||||||
|
{this.state.currentState?.manifest?.artist} - {this.state.currentState?.manifest?.album}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="player_controller_controls">
|
||||||
|
<Button
|
||||||
|
type="ghost"
|
||||||
|
shape="round"
|
||||||
|
icon={<Icons.ChevronLeft />}
|
||||||
|
onClick={this.onClickPreviousButton}
|
||||||
|
disabled={this.state.currentState?.syncModeLocked}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="playButton"
|
||||||
|
type="primary"
|
||||||
|
shape="circle"
|
||||||
|
icon={this.state.currentState?.playbackStatus === "playing" ? <Icons.MdPause /> : <Icons.MdPlayArrow />}
|
||||||
|
onClick={this.onClickTooglePlayButton}
|
||||||
|
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 className="player_controller_progress_wrapper">
|
||||||
|
<div
|
||||||
|
className="player_controller_progress"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
this.setState({
|
||||||
|
dragging: true,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onMouseUp={(e) => {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect()
|
||||||
|
const seekTime = this.state.currentState?.duration * (e.clientX - rect.left) / rect.width
|
||||||
|
|
||||||
|
this.onDragEnd(seekTime)
|
||||||
|
}}
|
||||||
|
onMouseMove={(e) => {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect()
|
||||||
|
const atWidth = (e.clientX - rect.left) / rect.width * 100
|
||||||
|
|
||||||
|
this.setState({ currentDragWidth: atWidth })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="player_controller_progress_bar"
|
||||||
|
style={{
|
||||||
|
width: `${this.state.dragging ? this.state.currentDragWidth : this.state.currentState?.time / this.state.currentState?.duration * 100}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SyncLyrics extends React.Component {
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
notAvailable: false,
|
||||||
|
|
||||||
|
currentManifest: null,
|
||||||
|
currentStatus: null,
|
||||||
|
|
||||||
|
canvas_url: null,
|
||||||
|
lyrics: null,
|
||||||
|
currentLine: null,
|
||||||
|
|
||||||
|
colorAnalysis: null,
|
||||||
|
|
||||||
|
classnames: [
|
||||||
|
{
|
||||||
|
name: "cinematic-mode",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "centered-player",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "video-canvas-enabled",
|
||||||
|
enabled: false,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
visualizerRef = React.createRef()
|
||||||
|
|
||||||
|
videoCanvasRef = React.createRef()
|
||||||
|
|
||||||
|
thumbnailCanvasRef = React.createRef()
|
||||||
|
|
||||||
|
events = {
|
||||||
|
"player.current.update": (currentManifest) => {
|
||||||
|
this.setState({ currentManifest })
|
||||||
|
|
||||||
|
if (document.startViewTransition) {
|
||||||
|
document.startViewTransition(this.loadLyrics)
|
||||||
|
} else {
|
||||||
|
this.loadLyrics()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"player.coverColorAnalysis.update": (colorAnalysis) => {
|
||||||
|
this.setState({ colorAnalysis })
|
||||||
|
},
|
||||||
|
"player.status.update": (currentStatus) => {
|
||||||
|
this.setState({ currentStatus })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isCurrentLine = (line) => {
|
||||||
|
if (!this.state.currentLine) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.state.currentLine.startTimeMs === line.startTimeMs
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLyrics = async () => {
|
||||||
|
if (this.syncInterval) {
|
||||||
|
clearInterval(this.syncInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.currentManifest) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading: true,
|
||||||
|
notAvailable: false,
|
||||||
|
lyrics: null,
|
||||||
|
currentLine: null,
|
||||||
|
canvas_url: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const api = app.cores.api.instance().instances.music
|
||||||
|
|
||||||
|
let response = await request({
|
||||||
|
instance: api,
|
||||||
|
method: "get",
|
||||||
|
url: `/lyrics/${this.state.currentManifest._id}`,
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
notAvailable: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {}
|
||||||
|
})
|
||||||
|
|
||||||
|
let data = response.data ?? {
|
||||||
|
lines: [],
|
||||||
|
syncType: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(this.state.currentManifest)
|
||||||
|
console.log(data)
|
||||||
|
|
||||||
|
if (data.lines.length > 0 && data.syncType === "LINE_SYNCED") {
|
||||||
|
data.lines = data.lines.map((line, index) => {
|
||||||
|
const ref = React.createRef()
|
||||||
|
|
||||||
|
line.ref = ref
|
||||||
|
|
||||||
|
line.startTimeMs = Number(line.startTimeMs)
|
||||||
|
|
||||||
|
const nextLine = data.lines[index + 1]
|
||||||
|
|
||||||
|
// calculate end time
|
||||||
|
line.endTimeMs = nextLine ? Number(nextLine.startTimeMs) : Math.floor(app.cores.player.duration() * 1000)
|
||||||
|
|
||||||
|
return line
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.canvas_url) {
|
||||||
|
//app.message.info("Video canvas loaded")
|
||||||
|
this.toogleVideoCanvas(true)
|
||||||
|
} else {
|
||||||
|
//app.message.info("No video canvas available for this song")
|
||||||
|
this.toogleVideoCanvas(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if has no lyrics or are unsynced, toogle cinematic mode off and center controller
|
||||||
|
if (data.lines.length === 0 || data.syncType !== "LINE_SYNCED") {
|
||||||
|
//app.message.info("No lyrics available for this song")
|
||||||
|
|
||||||
|
this.toogleCinematicMode(false)
|
||||||
|
this.toogleCenteredControllerMode(true)
|
||||||
|
} else {
|
||||||
|
//app.message.info("Lyrics loaded, starting sync...")
|
||||||
|
|
||||||
|
this.toogleCenteredControllerMode(false)
|
||||||
|
this.startLyricsSync()
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform times
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
syncType: data.syncType,
|
||||||
|
canvas_url: data.canvas_url,
|
||||||
|
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 = () => {
|
||||||
|
// create interval to sync lyrics
|
||||||
|
if (this.syncInterval) {
|
||||||
|
clearInterval(this.syncInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll to top
|
||||||
|
this.visualizerRef.current.scrollTop = 0
|
||||||
|
|
||||||
|
this.syncInterval = setInterval(() => {
|
||||||
|
if (!this.state.lyrics || !Array.isArray(this.state.lyrics) || this.state.lyrics.length === 0 || !this.state.lyrics[0]) {
|
||||||
|
console.warn(`Clearing interval because lyrics is not found or lyrics is empty, probably because memory leak or unmounted component`)
|
||||||
|
clearInterval(this.syncInterval)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const { time } = app.cores.player.currentState()
|
||||||
|
|
||||||
|
// transform audio seek time to lyrics time (ms from start) // remove decimals
|
||||||
|
const transformedTime = Math.floor(time * 1000)
|
||||||
|
|
||||||
|
const hasStartedFirst = transformedTime >= this.state.lyrics[0].startTimeMs
|
||||||
|
|
||||||
|
if (!hasStartedFirst) {
|
||||||
|
if (this.state.canvas_url) {
|
||||||
|
this.toogleCinematicMode(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the closest line to the transformed time
|
||||||
|
const line = this.state.lyrics.find((line) => {
|
||||||
|
// match the closest line to the transformed time
|
||||||
|
return transformedTime >= line.startTimeMs && transformedTime <= line.endTimeMs
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!line || !line.ref) {
|
||||||
|
console.warn(`Clearing interval because cannot find line to sync or line REF is not found, probably because memory leak or unmounted component`)
|
||||||
|
clearInterval(this.syncInterval)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line) {
|
||||||
|
if (this.isCurrentLine(line)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// set current line
|
||||||
|
this.setState({
|
||||||
|
currentLine: line,
|
||||||
|
})
|
||||||
|
|
||||||
|
//console.log(line)
|
||||||
|
|
||||||
|
if (!line.ref.current) {
|
||||||
|
console.log(line)
|
||||||
|
console.warn(`Clearing interval because line CURRENT ref is not found, probably because memory leak or unmounted component`)
|
||||||
|
clearInterval(this.syncInterval)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.visualizerRef.current.scrollTo({
|
||||||
|
top: line.ref.current.offsetTop - (this.visualizerRef.current.offsetHeight / 2),
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.state.canvas_url) {
|
||||||
|
if (line.words === "♪" || line.words === "♫" || line.words === " " || line.words === "") {
|
||||||
|
this.toogleCinematicMode(true)
|
||||||
|
} else {
|
||||||
|
this.toogleCinematicMode(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
// register player events
|
||||||
|
for (const [event, callback] of Object.entries(this.events)) {
|
||||||
|
app.eventBus.on(event, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get current playback status and time
|
||||||
|
const {
|
||||||
|
manifest,
|
||||||
|
playbackStatus,
|
||||||
|
colorAnalysis,
|
||||||
|
} = app.cores.player.currentState()
|
||||||
|
|
||||||
|
await this.setState({
|
||||||
|
currentManifest: manifest,
|
||||||
|
currentStatus: playbackStatus,
|
||||||
|
colorAnalysis,
|
||||||
|
})
|
||||||
|
|
||||||
|
app.SidebarController.toggleVisibility(false)
|
||||||
|
app.cores.style.compactMode(true)
|
||||||
|
app.cores.style.applyVariant("dark")
|
||||||
|
app.layout.floatingStack.toogleGlobalVisibility(false)
|
||||||
|
|
||||||
|
window._hacks = {
|
||||||
|
toogleVideoCanvas: this.toogleVideoCanvas,
|
||||||
|
toogleCinematicMode: this.toogleCinematicMode,
|
||||||
|
toogleCenteredControllerMode: this.toogleCenteredControllerMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadLyrics()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
// unregister player events
|
||||||
|
for (const [event, callback] of Object.entries(this.events)) {
|
||||||
|
app.eventBus.off(event, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear sync interval
|
||||||
|
if (this.syncInterval) {
|
||||||
|
clearInterval(this.syncInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete window._hacks
|
||||||
|
|
||||||
|
app.SidebarController.toggleVisibility(true)
|
||||||
|
app.cores.style.compactMode(false)
|
||||||
|
app.cores.style.applyInitialVariant()
|
||||||
|
app.layout.floatingStack.toogleGlobalVisibility(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLines() {
|
||||||
|
if (!this.state.lyrics || this.state.notAvailable || this.state.syncType !== "LINE_SYNCED") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.state.lyrics.map((line, index) => {
|
||||||
|
return <div
|
||||||
|
ref={line.ref}
|
||||||
|
className={classnames(
|
||||||
|
"lyrics_viewer_lines_line",
|
||||||
|
{
|
||||||
|
["current"]: this.isCurrentLine(line)
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
id={line.startTimeMs}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<h2>
|
||||||
|
{line.words}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div
|
||||||
|
ref={this.visualizerRef}
|
||||||
|
className={classnames(
|
||||||
|
"lyrics_viewer",
|
||||||
|
{
|
||||||
|
["text_dark"]: this.state.colorAnalysis?.isDark ?? false,
|
||||||
|
...this.state.classnames.map((classname) => {
|
||||||
|
return {
|
||||||
|
[classname.name]: classname.enabled,
|
||||||
|
}
|
||||||
|
}).reduce((a, b) => {
|
||||||
|
return {
|
||||||
|
...a,
|
||||||
|
...b,
|
||||||
|
}
|
||||||
|
}, {}),
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
"--predominant-color": this.state.colorAnalysis?.hex ?? "unset",
|
||||||
|
"--predominant-color-rgb-values": this.state.colorAnalysis?.value ? composeRgbValues(this.state.colorAnalysis?.value) : [0, 0, 0],
|
||||||
|
"--line-time": `${calculateLineTime(this.state.currentLine)}ms`,
|
||||||
|
"--line-animation-play-state": this.state.currentStatus === "playing" ? "running" : "paused",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="lyrics_viewer_mask"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="lyrics_viewer_video_canvas"
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
src={this.state.canvas_url}
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
controls={false}
|
||||||
|
ref={this.videoCanvasRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="lyrics_viewer_thumbnail"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={this.state.currentManifest?.thumbnail}
|
||||||
|
ref={this.thumbnailRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PlayerController />
|
||||||
|
|
||||||
|
<div className="lyrics_viewer_content">
|
||||||
|
<div className="lyrics_viewer_lines">
|
||||||
|
{
|
||||||
|
this.renderLines()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
472
packages/app/src/pages/lyrics/index.less
Normal file
472
packages/app/src/pages/lyrics/index.less
Normal file
@ -0,0 +1,472 @@
|
|||||||
|
@enabled-video-canvas-opacity: 0.4;
|
||||||
|
|
||||||
|
.lyrics_viewer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
isolation: isolate;
|
||||||
|
|
||||||
|
//align-items: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
padding: 50px 0;
|
||||||
|
|
||||||
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
background-color: rgba(var(--predominant-color-rgb-values), 0.8);
|
||||||
|
background:
|
||||||
|
linear-gradient(20deg, rgba(var(--predominant-color-rgb-values), 0.8), rgba(var(--predominant-color-rgb-values), 0.2)),
|
||||||
|
url(https://grainy-gradients.vercel.app/noise.svg);
|
||||||
|
|
||||||
|
//background-size: 1%;
|
||||||
|
background-position: center;
|
||||||
|
|
||||||
|
&.video-canvas-enabled {
|
||||||
|
.lyrics_viewer_video_canvas {
|
||||||
|
video {
|
||||||
|
opacity: @enabled-video-canvas-opacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.centered-player {
|
||||||
|
.lyrics_viewer_thumbnail {
|
||||||
|
width: 100vw;
|
||||||
|
|
||||||
|
height: 80vh; //fallback
|
||||||
|
height: 80dvh;
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player_controller_wrapper {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.player_controller {
|
||||||
|
margin-top: 40%;
|
||||||
|
|
||||||
|
max-width: 50vw;
|
||||||
|
max-height: 50vh;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
//height: 100%;
|
||||||
|
|
||||||
|
border-radius: 18px;
|
||||||
|
|
||||||
|
gap: 50px;
|
||||||
|
|
||||||
|
.player_controller_cover {
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player_controller_info {
|
||||||
|
.player_controller_info_title {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cinematic-mode {
|
||||||
|
.lyrics_viewer_mask {
|
||||||
|
backdrop-filter: blur(0px);
|
||||||
|
-webkit-backdrop-filter: blur(0px)
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics_viewer_background {
|
||||||
|
video {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics_viewer_content {
|
||||||
|
.lyrics_viewer_lines {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.text_dark {
|
||||||
|
.lyrics_viewer_content {
|
||||||
|
.lyrics_viewer_lines {
|
||||||
|
.lyrics_viewer_lines_line {
|
||||||
|
color: var(--text-color-white);
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: var(--text-color-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics_viewer_mask {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
z-index: 200;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
backdrop-filter: blur(21px);
|
||||||
|
-webkit-backdrop-filter: blur(21px)
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics_viewer_video_canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
//height: 100dvh;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics_viewer_thumbnail {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
z-index: 250;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 25vw;
|
||||||
|
height: 25vw;
|
||||||
|
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 500px;
|
||||||
|
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics_viewer_content {
|
||||||
|
z-index: 250;
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
.lyrics_viewer_lines {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 90%;
|
||||||
|
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
font-family: "Space Grotesk", sans-serif;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
&::after,
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
//height: 50dvh;
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lyrics_viewer_lines_line {
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
z-index: 250;
|
||||||
|
|
||||||
|
&.current {
|
||||||
|
margin: 20px 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
|
||||||
|
animation: spacing-letters var(--line-time) ease-in-out forwards;
|
||||||
|
animation-play-state: var(--line-animation-play-state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spacing-letters {
|
||||||
|
0% {
|
||||||
|
letter-spacing: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player_controller_wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
margin: 50px;
|
||||||
|
|
||||||
|
z-index: 350;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
.player_controller {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 25vw;
|
||||||
|
|
||||||
|
min-width: 350px;
|
||||||
|
max-width: 500px;
|
||||||
|
|
||||||
|
height: 220px;
|
||||||
|
|
||||||
|
background-color: rgba(var(--background-color-accent-values), 0.4);
|
||||||
|
// background:
|
||||||
|
// linear-gradient(20deg, rgba(var(--background-color-accent-values), 0.8), transparent),
|
||||||
|
// url(https://grainy-gradients.vercel.app/noise.svg);
|
||||||
|
|
||||||
|
-webkit-backdrop-filter: blur(21px);
|
||||||
|
backdrop-filter: blur(21px);
|
||||||
|
|
||||||
|
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.player_controller--hovering {
|
||||||
|
.player_controller_controls {
|
||||||
|
height: 8vh;
|
||||||
|
max-height: 100px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player_controller_progress_wrapper {
|
||||||
|
bottom: 7px;
|
||||||
|
|
||||||
|
.player_controller_progress {
|
||||||
|
height: 10px;
|
||||||
|
|
||||||
|
width: 90%;
|
||||||
|
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player_controller_cover {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 10vw;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player_controller_left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
.player_controller_info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
.player_controller_info_title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
color: var(--background-color-contrast)
|
||||||
|
}
|
||||||
|
|
||||||
|
.player_controller_info_artist {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player_controller_controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
height: 0px;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player_controller_progress_wrapper {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.player_controller_progress {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 5px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.player_controller_progress_bar {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background-color: var(--background-color-contrast);
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user