369 lines
7.9 KiB
JavaScript
369 lines
7.9 KiB
JavaScript
import { LitElement, css, html, unsafeCSS } from 'lit'
|
|
import { Task } from '@lit-labs/task'
|
|
import WaveSurfer from 'wavesurfer.js'
|
|
import { formatSeconds } from '../api/utils'
|
|
|
|
import MainCSS from '../assets/styles/main.scss?inline'
|
|
|
|
// temp bg image
|
|
import Photo from '/images/Meredith Monk (1974) Photo Lauretta HArris.jpg?url'
|
|
|
|
// components
|
|
import './SvgIcon.js'
|
|
import './RangeSlider.js'
|
|
|
|
class AudioPlayer extends LitElement {
|
|
static properties = {
|
|
details: { type: Object },
|
|
data: { state: true },
|
|
playing: { state: true },
|
|
volume: { state: true },
|
|
track: { state: true },
|
|
audio: { state: true },
|
|
}
|
|
|
|
constructor() {
|
|
super()
|
|
this.details = {}
|
|
this.data = {}
|
|
this.audio = null
|
|
this.playing = false
|
|
this.audioEl = {}
|
|
|
|
this.track = 0
|
|
}
|
|
|
|
_getAudio = new Task(
|
|
this,
|
|
async () => {
|
|
if (!this.details.media) return
|
|
|
|
if (this.details.tracks) {
|
|
console.log(this.details.tracks[0])
|
|
|
|
if (!this.audio) {
|
|
this.initAudio()
|
|
}
|
|
|
|
this.audio.load(this.details.tracks[this.track])
|
|
|
|
return this.audio
|
|
} else {
|
|
try {
|
|
if (this.audio.media) {
|
|
this.audio.destroy()
|
|
}
|
|
|
|
const wsEl = this.initWs()
|
|
|
|
this.audio.load(`/media/${this.details.media}.mp3`, this.details.data)
|
|
|
|
return wsEl
|
|
} catch (err) {
|
|
console.error(err)
|
|
}
|
|
}
|
|
},
|
|
() => [ this.details ]
|
|
)
|
|
|
|
initAudio() {
|
|
this.audio = document.createElement('audio')
|
|
|
|
|
|
this.audio.addEventListener('loadstart', () => {
|
|
this.loading = true
|
|
})
|
|
|
|
this.audio.addEventListener('canplay', () => {
|
|
this.loading = false
|
|
})
|
|
|
|
this.audio.addEventListener('play', () => {
|
|
this.playing = true
|
|
})
|
|
|
|
this.audio.addEventListener('pause', () => {
|
|
this.playing = false
|
|
})
|
|
|
|
this.audio.addEventListener('loadedmetadata', () => {
|
|
this.data = {
|
|
...this.data,
|
|
position: 0,
|
|
duration: this.audio.duration
|
|
}
|
|
})
|
|
|
|
this.audio.addEventListener('ended', () => {
|
|
// auto play next track or end if last track
|
|
})
|
|
|
|
this.audio.addEventListener('timeupdate', () => {
|
|
this.data = {
|
|
...this.data,
|
|
position: this.audio.currentTime
|
|
}
|
|
})
|
|
|
|
this.audio.addEventListener('seeking', () => {
|
|
this.data = {
|
|
...this.data,
|
|
position: this.audio.currentTime
|
|
}
|
|
})
|
|
}
|
|
|
|
initWs() {
|
|
const div = document.createElement('div')
|
|
div.classList.add('waveform')
|
|
|
|
this.audio = WaveSurfer.create({
|
|
container: div,
|
|
waveColor: '#ffffff50',
|
|
progressColor: 'white',
|
|
cursorColor: 'transparent',
|
|
fillParent: true,
|
|
barWidth: 4,
|
|
barGap: 6,
|
|
barRadius: 5,
|
|
dragToSeek: true,
|
|
})
|
|
|
|
this.audio.setVolume(0.6)
|
|
this.volume = this.audio.getVolume()
|
|
|
|
this.audio.on('ready', (duration) => {
|
|
this.data = {
|
|
...this.data,
|
|
position: 0,
|
|
duration
|
|
}
|
|
})
|
|
|
|
this.audio.on('play', () => {
|
|
this.playing = true
|
|
})
|
|
|
|
this.audio.on('pause', () => {
|
|
this.playing = false
|
|
})
|
|
|
|
this.audio.on('interaction', (newTime) => {
|
|
this.data = {
|
|
...this.data,
|
|
position: newTime
|
|
}
|
|
})
|
|
|
|
this.audio.on('timeupdate', (currentTime) => {
|
|
this.data = {
|
|
...this.data,
|
|
position: currentTime
|
|
}
|
|
})
|
|
|
|
return div
|
|
}
|
|
|
|
togglePlay() {
|
|
this.audio.playPause()
|
|
}
|
|
|
|
skipForward() {
|
|
this.audio.skip(15)
|
|
}
|
|
|
|
skipBackward() {
|
|
this.audio.skip(-15)
|
|
}
|
|
|
|
nextTrack() {
|
|
this.dispatchEvent(new Event('next-track', { bubbles: true, composed: true }))
|
|
}
|
|
|
|
prevTrack() {
|
|
this.dispatchEvent(new CustomEvent('prev-track', { bubbles: true, composed: true }))
|
|
}
|
|
|
|
volHandler(evt) {
|
|
if (evt.detail) {
|
|
this.audio.setVolume(evt.detail.value)
|
|
}
|
|
}
|
|
|
|
progressCalc() {
|
|
const { position, duration } = this.data
|
|
let percentage = 0
|
|
|
|
if (position && duration) {
|
|
percentage = (position / duration) * 100
|
|
}
|
|
|
|
return `${percentage}%`
|
|
}
|
|
|
|
render() {
|
|
return html`
|
|
<img class="bg-img" src=${Photo} />
|
|
<header>
|
|
<h2>${this.details.title}</h2>
|
|
<p class="details">${this.details.details}</p>
|
|
</header>
|
|
|
|
<div class="info">
|
|
<div class="progress">
|
|
<div class="bar" style="--progress-bar: ${this.progressCalc()}"></div>
|
|
</div>
|
|
|
|
${this._getAudio.render({
|
|
complete: (div) => div
|
|
})}
|
|
|
|
<span class="time_pos">${formatSeconds(this.data.position)}</span> <span class="time_dur">${formatSeconds(this.data.duration)}</span>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<div>
|
|
<mm-icon name="skip-prev" @click=${this.prevTrack} ?disabled=${!this.data.duration || this.getAudio?.status == 1}></mm-icon>
|
|
<mm-icon name="skip-b15" @click=${this.skipBackward} ?disabled=${!this.data.duration || this.getAudio?.status == 1} class="skip15"></mm-icon>
|
|
<mm-icon name="${this.playing ? 'pause' : 'play'}" @click=${this.togglePlay} ?disabled=${!this.data.duration || this.getAudio?.status == 1} class=${this.playing ? 'pause' : 'play'}></mm-icon>
|
|
<mm-icon name="skip-f15" @click=${this.skipForward} ?disabled=${!this.data.duration || this.getAudio?.status == 1} class="skip15"></mm-icon>
|
|
<mm-icon name="skip-next" @click=${this.nextTrack} ?disabled=${!this.data.duration || this.getAudio?.status == 1}></mm-icon>
|
|
</div>
|
|
|
|
<mm-range
|
|
value=${this.volume}
|
|
@input=${this.volHandler}
|
|
?disabled=${!this.data.duration || this.getAudio?.status == 1}
|
|
style="--track-height: 0.4em; --thumb-size: 1.2em; --track-color: #ffffff50"
|
|
></mm-range>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
static styles = [ css`${unsafeCSS(MainCSS)}`, css`
|
|
:host {
|
|
position: absolute;
|
|
inset: 0;
|
|
padding: 1em;
|
|
display: grid;
|
|
grid-template-rows: 0.7fr 0.7fr 1fr;
|
|
gap: 1em;
|
|
opacity: 0;
|
|
animation: fadeIn 0.125s ease-in forwards;
|
|
overflow: hidden;
|
|
border-radius: 0.75em 0 0 0.75em;
|
|
isolation: isolate;
|
|
}
|
|
|
|
.bg-img {
|
|
position: absolute;
|
|
inset: 0;
|
|
object-fit: cover;
|
|
object-position: center 25%;
|
|
opacity: 0.2;
|
|
mix-blend-mode: overlay;
|
|
scale: 1.1;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
header {
|
|
color: var(--netural-100, white);
|
|
font-size: 1.5em;
|
|
line-height: 1.2;
|
|
padding: 0.5em;
|
|
}
|
|
|
|
header > p {
|
|
font-size: 1.5em;
|
|
max-width: 25ch;
|
|
}
|
|
|
|
.info, .controls {
|
|
position: relative;
|
|
}
|
|
|
|
.progress {
|
|
--progress-bar: 0%;
|
|
position: absolute;
|
|
display: flex;
|
|
align-items: center;
|
|
inset: 0;
|
|
z-index: 3;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.progress > .bar {
|
|
height: 0.25em;
|
|
width: var(--progress-bar);
|
|
background: #ffffff80;
|
|
border-radius: 100vmax;
|
|
}
|
|
|
|
.time_pos, .time_dur {
|
|
font-family: sans-serif;
|
|
font-size: 0.85em;
|
|
color: var(--neutral-100, white);
|
|
position: absolute;
|
|
bottom: 1em;
|
|
}
|
|
|
|
.time_dur {
|
|
right: 0;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-evenly;
|
|
width: 65%;
|
|
margin-inline: auto;
|
|
}
|
|
|
|
.controls > div {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-evenly;
|
|
margin-block-end: 1em;
|
|
gap: 0.25em;
|
|
}
|
|
|
|
mm-icon {
|
|
color: white;
|
|
font-size: 2.5em;
|
|
}
|
|
|
|
mm-icon.skip15 {
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
mm-icon:not(.skip15, .play, .pause) {
|
|
font-size: 1em;
|
|
}
|
|
|
|
mm-volctrl {
|
|
display: block;
|
|
width: 80%;
|
|
margin-inline: auto;
|
|
}
|
|
|
|
.waveform {
|
|
position: relative;
|
|
height: 15em;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
` ]
|
|
}
|
|
|
|
customElements.define('mm-aplayer', AudioPlayer)
|