mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-10 02:54:15 +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