various updates across components and views

This commit is contained in:
suroh 2023-10-06 14:06:37 +02:00
parent 86dbe77be9
commit 97de4f3bbb
8 changed files with 152 additions and 534 deletions

View File

@ -1,368 +0,0 @@
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)

View File

@ -32,6 +32,7 @@ class HorizontalScroller extends LitElement {
scroll-padding-inline: 0.5em;
padding-inline-start: 0.5em;
padding-inline-end: 0.5em;
padding-block-start: 0.25em;
padding-block-end: 0.75em;
}
`

View File

@ -4,44 +4,84 @@ import MainCSS from '../assets/styles/main.scss?inline'
class ImageViewer extends LitElement {
static properties = {
src: { type: String },
details: { type: Object },
imgEl: { state: true },
figEl: { state: true },
captionWidth: { state: true },
loading: { state: true }
loading: { state: true },
title: { state: true },
caption : { state: true }
}
constructor() {
super()
this.src = ''
this.details = {}
this.imgEl = new Image()
this.captionWidth = 'min-content'
this.loading = true
this.title = ''
this.caption = ''
this.figEl = {}
}
firstUpdated() {
this.figEl = this.shadowRoot.querySelector('figure')
}
connectedCallback() {
super.connectedCallback()
this.imgEl.addEventListener('load', () => {
this.figEl.classList.remove('unloading')
this.captionWidth = `${this.imgEl.offsetWidth}px`
this.loading = false
})
}
set src(val) {
if (val) {
this.imgEl.src = val
console.log(this.imgEl)
willUpdate(att) {
if (att.has('details') && this.details?.media) {
this.figEl.classList.add('unloading')
this.figEl.addEventListener('transitionend', () => {
this.title = this.details.title || ''
this.caption = this.details.caption || ''
this.imgEl.hidden = false
this.imgEl.src = this.details.media
})
}
this.update()
}
fullscreen() {
if (this.figEl.webkitSupportsFullscreen) {
this.figEl.webkitEnterFullscreen()
return
}
if (document.fullscreenElement !== null) {
document.exitFullscreen()
this.figEl.removeAttribute('data-fullscreen')
} else {
this.figEl.requestFullscreen()
this.figEl.setAttribute('data-fullscreen', true)
}
}
render() {
return html`
<figure class="${this.loading ? 'loading' : ''}">
<figure class="" @click=${this.fullscreen}>
<picture>
${this.imgEl}
${this.oldEl}
</picture>
<figcaption style="--width: ${this.captionWidth}">
Image Description
<p class="title">${this.title}</p>
${this.caption ? html`<p class="sub">${this.caption}</p>` : ''}
</figcaption>
</figure>
`
@ -54,10 +94,14 @@ class ImageViewer extends LitElement {
display: grid;
grid-template-rows: 1fr 4em;
justify-content: center;
transition: opacity 0.5s ease;
transition: opacity 0.125s 0.125s ease-in-out;
}
.loading {
figure[data-fullscreen] {
color: white;
}
.loading, .unloading {
opacity: 0;
}

View File

@ -1,7 +1,6 @@
import { LitElement, html, css,unsafeCSS } from 'lit'
import { Task } from '@lit-labs/task'
import { formatSeconds } from '../api/utils'
import WaveSurfer from 'wavesurfer.js'
import Photo from '/images/Meredith Monk (1974) Photo Lauretta Harris.jpg'
import MainCSS from '../assets/styles/main.scss?inline'
@ -31,7 +30,6 @@ class ModularPlayer extends LitElement {
super()
this.details = {}
this.audio = null
this.ws = null
this.track = 0
this.position = 0
@ -44,27 +42,23 @@ class ModularPlayer extends LitElement {
_getTrack = new Task(
this,
async () => {
if (this.details.tracks) {
this.audio.src = `/media${this.details.media}/${this.details.tracks[this.track]}`
} else {
this.audio.src = `/media${this.details.media}.mp3`
}
this.audio.load()
if (this.playing && this.audio.paused) {
this.audio.play()
try {
if (this.details) {
if (this.details?.tracks) {
this.audio.src = `/media${this.details.media}/${this.details.tracks[this.track]}`
} else {
this.audio.src = `/media${this.details.media}.mp3`
}
this.audio.load()
if (this.playing && this.audio.paused) {
this.audio.play()
}
}
} catch (err) {
console.error(err)
}
},
() => [ this.track ]
)
_renderWave = new Task(
this,
{
task: async () => {
return await this.initWs(this.details.data)
},
autoRun: false
}
() => [ this.track, this.details ]
)
initAudio() {
@ -97,10 +91,6 @@ class ModularPlayer extends LitElement {
this.audio.addEventListener('loadedmetadata', () => {
this.position = 0
this.duration = this.audio.duration
if (this.duration > 0) {
this._renderWave.run()
}
})
this.audio.addEventListener('ended', () => {
@ -127,40 +117,6 @@ class ModularPlayer extends LitElement {
})
}
initWs(peakData) {
return new Promise((go, no) => {
try {
const div = document.createElement('div')
div.classList.add('waveform')
this.ws = WaveSurfer.create({
container: div,
waveColor: '#ffffff50',
progressColor: 'white',
cursorColor: 'transparent',
fillParent: true,
normalize: true,
barWidth: 4,
barGap: 6,
barRadius: 5,
dragToSeek: true,
media: this.audio,
peaks: peakData
})
this.ws.on('interaction', (currentTime) => {
this.position = currentTime
})
this.ws.on('ready', () => {
setTimeout(() => go(div), 500)
})
} catch (err) {
no(new Error(err))
}
})
}
togglePlay() {
if (this.audio.paused) {
this.playing = true
@ -231,7 +187,8 @@ class ModularPlayer extends LitElement {
render() {
return html`
<div class="player">
${ this.details ?
html`<div class="player">
${this.details.tracks ? html`
<div class="tracklist">
<h2>${this.details.title}</h2>
@ -258,28 +215,30 @@ class ModularPlayer extends LitElement {
complete: () => html`
<span class="time_pos">${formatSeconds(this.position)}</span>
${!this.details.tracks ?
this._renderWave.render({
initial: () => html`<mm-loading class="waveform" nobg></mm-loading>`,
pending: () => html`<mm-loading class="waveform" nobg></mm-loading>`,
complete: (ws) => html`
<div class="progress">
<div class="bar" style="--progress-bar: ${this.progressCalc()}">
<div class="blob"></div>
</div>
</div>
${ws}`
}) :
${this.details?.tracks ?
html`<mm-range
value=${this.position}
max=${this.duration}
step="0.1"
@input=${this.seekTrack}
></mm-range>`
: html`
<div class="waveform">
<mm-range
value=${this.position}
max=${this.duration}
step="0.1"
@input=${this.seekTrack}
class="progress"
></mm-range>
<img class="wav-img" src="/media${this.details.media}.png">
</div>
`
}
<span class="time_dur">${formatSeconds(this.duration)}</span>
`
`,
error: (err) => err
})}
${this.audio}
@ -304,7 +263,7 @@ class ModularPlayer extends LitElement {
</div>
</div>
</div>
`
` : ''}`
}
static styles = [ css`${unsafeCSS(MainCSS)}`, css`
@ -456,7 +415,6 @@ class ModularPlayer extends LitElement {
gap: 1em;
align-items: center;
& > mm-range {
flex-grow: 1;
--track-height: 0.4em;
@ -472,7 +430,7 @@ class ModularPlayer extends LitElement {
& .time_pos,
& .time_dur {
position: absolute;
bottom: var(--padding);
bottom: 0;
}
& .time_dur {
@ -522,34 +480,15 @@ class ModularPlayer extends LitElement {
}
.progress {
--progress-bar: 0%;
--track-height: 0.25em;
--track-color: transparent;
--thumb-size: 0.75em;
--track-progress: #ffffff50;
position: absolute;
display: flex;
align-items: center;
inset: 0;
z-index: 3;
pointer-events: none;
padding-inline: var(--padding);
> .bar {
--bar-height: 0.25em;
height: var(--bar-height);
width: var(--progress-bar);
background: #ffffff80;
border-radius: 100vmax;
position: relative;
> .blob {
position: absolute;
top: calc(var(--bar-height) * -0.5);
right: calc(var(--bar-height) * -1);
width: 0.5em;
height: 0.5em;
border-radius: 100vmax;
background: white;
}
}
}
mm-icon {
@ -566,11 +505,17 @@ class ModularPlayer extends LitElement {
}
.waveform {
display: block;
position: absolute;
inset: 0 var(--padding);
display: flex;
flex-direction: column;
justify-content: center;
> .wav-img {
display: block;
max-width: 100%;
}
}
@keyframes fadeIn {

View File

@ -1,6 +1,7 @@
import { LitElement, html, css, unsafeCSS } from 'lit'
import MainCSS from '../assets/styles/main.scss?inline'
import Router from '../api/Router'
class VerticalCard extends LitElement {
static properties = {
@ -29,7 +30,7 @@ class VerticalCard extends LitElement {
return html`
<div @click=${this.select} class=${this.selected ? 'selected' : ''}>
<picture>
<img src="/media/documentaries_interviews/thumbs/girlchild.png">
<img src="/media/${Router.route.path}/thumbs/${this.details.media}">
</picture>
<aside>
<p class="title">${this.details?.title}</p>

View File

@ -27,8 +27,6 @@ class AudioView extends LitElement {
firstUpdated() {
this.addEventListener('select-audio', ({ detail }) => {
this.selected = { ...detail, media: `${Router.route.path}/${detail.media}` }
console.log(this.selected)
this._selectAudio.run()
})
this.addEventListener('next-track', () => {
@ -54,23 +52,11 @@ class AudioView extends LitElement {
() => [ this.path ]
)
_selectAudio = new Task(
this,
{
task: async ([ selected ]) => {
const res = await fetch(`/media${selected.media}.json`)
return { ...selected, ...await res.json() }
},
args: () => [ this.selected ]
}
)
_skipTrack(direction) {
const idx = (this.selected.idx + direction) % this.tracks.length
const detail = { ...this.tracks[idx], idx }
this.selected = { ...detail, media: `${Router.route.path}/${detail.media}` }
this._selectAudio.run()
}
render() {
@ -90,10 +76,7 @@ class AudioView extends LitElement {
<div class="player">
<div>
${this._selectAudio.render({
pending: () => html`<mm-loading style="--fill-color: grey"></mm-loading>`,
complete: (selected) => html`<mm-audio-player .details=${selected}></mm-audio-player>`
})}
<mm-audio-player .details=${this.selected}></mm-audio-player>
</div>
</div>
</main>

View File

@ -1,4 +1,8 @@
import { LitElement, css, html } from "lit";
import { LitElement, css, html, unsafeCSS } from 'lit'
import { Task } from '@lit-labs/task'
import Router from '../api/Router.js'
import MainCSS from '../assets/styles/main.scss'
import '../components/Header.js'
import '../components/Footer.js'
@ -7,10 +11,38 @@ import '../components/VerticalCard.js'
import '../components/ImageViewer.js'
class ImageView extends LitElement {
static properties = {}
static properties = {
images: { state: true },
selected: { state: true }
}
constructor() {
super()
this.images = []
}
_getImages = new Task(
this,
async () => {
try {
const res = await fetch('/data/images.json')
const json = await res.json()
this.images = json
this.selected = { ...this.images[0], media: `/media${Router.route.path}/${this.images[0].media}`}
} catch (err) {
console.error(err)
}
},
() => []
)
selectImage({ target }) {
const { details } = target
if (details?.title != this.selected?.title) {
this.selected = { ...details, media: `/media${Router.route.path}/${details.media}` }
}
}
render() {
@ -18,28 +50,20 @@ class ImageView extends LitElement {
<mm-header></mm-header>
<main>
<div class="image">
<mm-imageviewer src="https://picsum.photos/${parseInt((Math.random() * 400) + 300)}/${parseInt((Math.random() * 400) + 400)}"></mm-imageviewer>
<mm-imageviewer .details=${this.selected}></mm-imageviewer>
</div>
<mm-hscroller>
<mm-vcard .details=${{'title': 'Image'}}></mm-vcard>
<mm-vcard .details=${{'title': 'Image'}}></mm-vcard>
<mm-vcard .details=${{'title': 'Image'}}></mm-vcard>
<mm-vcard .details=${{'title': 'Image'}}></mm-vcard>
<mm-vcard .details=${{'title': 'Image'}}></mm-vcard>
<mm-vcard .details=${{'title': 'Image'}}></mm-vcard>
<mm-vcard .details=${{'title': 'Image'}}></mm-vcard>
<mm-vcard .details=${{'title': 'Image'}}></mm-vcard>
<mm-vcard .details=${{'title': 'Image'}}></mm-vcard>
<mm-vcard .details=${{'title': 'Image'}}></mm-vcard>
<mm-vcard .details=${{'title': 'Image'}}></mm-vcard>
${this._getImages.render({ complete: () => this.images.map(i => html`
<mm-vcard @click=${this.selectImage} .details=${i} ?selected=${i.title == this.selected?.title}></mm-vcard>
`) })}
</mm-hscroller>
</main>
<mm-footer></mm-footer>
`
}
static styles = css`
static styles = [ css`${unsafeCSS(MainCSS)}`, css`
:host {
display: grid;
grid-template-rows: min-content 1fr min-content;
@ -52,6 +76,10 @@ class ImageView extends LitElement {
--gap: 0.75em;
}
mm-vcard {
--color: var(--green-400, lime);
}
main {
display: grid;
grid-template-rows: 1fr auto;
@ -63,7 +91,7 @@ class ImageView extends LitElement {
inline-margin: auto;
height: 100%;
}
`
` ]
}
customElements.define('mm-images', ImageView)

View File

@ -41,32 +41,16 @@ class VideoPage extends LitElement {
() => [ this.data ]
)
// TODO@mx this might not be necessary
_selectVideo = new Task(
this,
{
task: () => {
// console.log(this.selected)
},
args: () => [ this.selected ],
}
)
render() {
return html`
<mm-header></mm-header>
<main>
${this._selectVideo.render({
pending: () => html`Loading...`,
complete: () => html`
<aside>
<h2>${this.selected.title}</h2>
<p class="detail">${this.selected.detail}</p>
</aside>
<mm-vplayer media='/media/${this.selected.media}' ?autoplay=${this.selected.autoplay}></mm-vplayer>
`
})}
<aside>
<h2>${this.selected.title}</h2>
<p class="detail">${this.selected.detail}</p>
</aside>
<mm-vplayer media='/media/${this.selected.media}' ?autoplay=${this.selected.autoplay}></mm-vplayer>
</main>
<mm-hscroller>