;(function () { const id = '95699143-a0ec-4af7-b957-aa1f19c86c39' const sessionId = 'b29dc5d2-8641-4f5b-96b1-f02d33ee5f3e' const apiUrl = 'https://sharefol.io/api' const data = [{"id":"g9","data":{"description":"Tracks with strong rhythms influenced by funk, fusion, and more.","theme":{}},"text":"Groove / Funk / Fusion","parent":"p4","children":[{"id":"t17","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":0,"loopTo":0,"fadeOut":0,"loopFrom":0,"loopPointType":"end"},"energy":{"max":244.9687185277871,"min":-243.13989684743797,"average":68.19054733830214},"artist":"Jamphibious","album":null,"duration":9830,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/pointlessmakeshiftShrew.dat","metadata":{"album":null,"albumArtist":null,"composer":null,"genre":null,"grouping":null,"releaseDate":null,"bpm":null,"isrc":null,"description":"Jingle mock-up for a power up or level complete."}},"text":"Powered UP","parent":"g9"},{"id":"t37","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":2,"loopTo":0,"fadeOut":1,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":"Jamphibian","duration":120000,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/5cc95c9c-9716-4890-ab9b-13746a5f5b2f.dat","metadata":{"album":"Jamphibian","albumArtist":null,"composer":"Jordan Michael Reed","genre":"Funk","grouping":null,"releaseDate":"TBA","bpm":"128","isrc":null,"description":"Battle music from the in development rhythm action game \"Jamphibian!\""}},"text":"Break Da Beat","parent":"g9"},{"id":"t48","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":0,"loopTo":0,"fadeOut":0,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":"Treasure Tech Land","duration":180912,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/61d60412-d778-4d4a-a10d-f6468b11833a.dat","metadata":{"album":"Treasure Tech Land","albumArtist":"Jamphibious","composer":"Jordan Michael Reed","genre":"Fusion","grouping":null,"releaseDate":null,"bpm":"154","isrc":null,"description":"Gameplay theme written for the DOOM 2 mod \"Treasure Tech Land\""}},"text":"Blue Sky Road","parent":"g9"},{"id":"t18","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":0,"loopTo":0,"fadeOut":0,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":"Little Frog Game OST","duration":82388,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/4e2977ce-650b-4bae-9d02-5e0fa9d99393.dat","metadata":{"album":"Little Frog Game OST","albumArtist":null,"composer":"Jordan Michael Reed","genre":"VGM","grouping":null,"releaseDate":null,"bpm":null,"isrc":null,"description":"Music from the 2nd stage of Little Frog Game with a beach theme."}},"text":"Frog on the Beach","parent":"g9"},{"id":"t20","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":0,"loopTo":0,"fadeOut":0,"loopFrom":0,"loopPointType":"end"},"energy":{"max":247.5057222205267,"min":-242.0815149388104,"average":111.42632137285676},"artist":"Jamphibious","album":"Quantum Qitty","duration":195009,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/hotwillingStingray.dat","metadata":{"album":"Quantum Qitty","albumArtist":"Jamphibious","composer":"Jordan Michael Reed","genre":"VGM","grouping":"Game Jams","releaseDate":null,"bpm":null,"isrc":null,"description":"Stage theme from Quantum Qitty - a game created during Ludum Dare 49."}},"text":"Quantum Quest","parent":"g9"}]},{"id":"g8","data":{"description":"Select video game works with intense, high octane feel.","theme":{}},"text":"Upbeat / Intense / Action","parent":"p4","children":[{"id":"t46","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":2,"loopTo":2.47,"fadeOut":1,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":"SHMUP Prototype","duration":68406,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/d4cac26b-bd60-4cdc-b61e-cabd727c95d5.dat","metadata":{"album":"SHMUP Prototype","albumArtist":null,"composer":"Jordan Michael Reed","genre":"Shmup","grouping":null,"releaseDate":null,"bpm":null,"isrc":null,"description":"Music written for a cancelled shmup prototype game."}},"text":"Starship Engage","parent":"g8"},{"id":"t8","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":2,"loopTo":5.21,"fadeOut":1,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":"White Clothes","duration":91304,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/b0ebb35b-7603-4039-9c4e-60f88b2a1fab.dat","metadata":{"album":"White Clothes","albumArtist":null,"composer":"Jordan Michael Reed","genre":"RPG","grouping":null,"releaseDate":null,"bpm":"184","isrc":null,"description":null}},"text":"Luck and Pluck","parent":"g8"},{"id":"t9","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":2,"loopTo":3.45,"fadeOut":1,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":"White Clothes","duration":126534,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/14b20b32-bbcb-448a-8260-2e5ea55659a2.dat","metadata":{"album":"White Clothes","albumArtist":null,"composer":"Jordan Michael Reed","genre":"RPG","grouping":null,"releaseDate":null,"bpm":"154","isrc":null,"description":null}},"text":"Face the Darkness","parent":"g8"},{"id":"t13","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":0,"loopTo":0,"fadeOut":0,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":"Fraymakers","duration":110142,"waveform":"https://sharefolio-public.s3.amazonaws.com/questionableoldWolf/bf1423dd-c201-4ca4-95f0-696eadf29bd6.dat","metadata":{"album":"Fraymakers","albumArtist":null,"composer":"Super Soul Bros","genre":"Electronic","grouping":null,"releaseDate":null,"bpm":null,"isrc":null,"description":"Official OCRemix version of the Main Theme for Fraymakers."}},"text":"Fraymakers Theme (Jamphibious Remix)","parent":"g8"},{"id":"t38","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":2,"loopTo":0,"fadeOut":1,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":"NOISZ STΔRLIVHT","duration":33488,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/cd2c96d0-deb2-4494-8af8-c17446111e85.dat","metadata":{"album":"NOISZ STΔRLIVHT","albumArtist":"Jamphibious","composer":"Jordan Michael Reed","genre":"Action","grouping":null,"releaseDate":null,"bpm":null,"isrc":null,"description":"Intense music written for visual novel story segments in NOISZ STΔRLIVHT."}},"text":"Battle in VR","parent":"g8"},{"id":"t40","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":0,"loopTo":0,"fadeOut":0,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":null,"duration":10008,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/27af8534-4311-46dd-b6cf-43da60711faf.dat","metadata":{"album":null,"albumArtist":null,"composer":"Jordan Michael Reed","genre":null,"grouping":null,"releaseDate":null,"bpm":"182","isrc":null,"description":"End of stage jingle created for a shmup demo."}},"text":"Mission Complete!","parent":"g8"}]},{"id":"g7","data":{"description":"Select video game works with slow, relaxed, or downtempo feel.","theme":{}},"text":"Chill / Downtempo / Moody","parent":"p4","children":[{"id":"t36","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":2,"loopTo":0,"fadeOut":1,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":"NOISZ STΔRLIVHT","duration":42666,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/b9f7bb1b-d9f3-4505-b3ef-82dba93edae6.dat","metadata":{"album":"NOISZ STΔRLIVHT","albumArtist":"Jamphibious","composer":"Jordan Michael Reed","genre":"Ambient","grouping":null,"releaseDate":null,"bpm":null,"isrc":null,"description":"Calm music written for visual novel story segments in NOISZ STΔRLIVHT."}},"text":"Calm River","parent":"g7"},{"id":"t44","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":2,"loopTo":0,"fadeOut":1,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":"Jester Knight","duration":101818,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/39e1b995-c5fb-456d-b5d9-a1075b07ac65.dat","metadata":{"album":"Jester Knight","albumArtist":null,"composer":"Jordan Michael Reed","genre":"Spooky","grouping":null,"releaseDate":null,"bpm":null,"isrc":null,"description":"Dungeon music from the in development game \"Jester Knight\""}},"text":"Slaughterhouse Dungeon","parent":"g7"},{"id":"t14","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":0,"loopTo":0,"fadeOut":0,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":null,"duration":135004,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/7d710fe1-bf45-4095-9042-f493c2091568.dat","metadata":{"album":null,"albumArtist":null,"composer":"Jordan Michael Reed","genre":"VGM","grouping":null,"releaseDate":null,"bpm":null,"isrc":null,"description":"Music created as a test piece for a sci-fi project."}},"text":"Unknown World","parent":"g7"},{"id":"t15","data":{"art":null,"theme":{"bgColor":"#333333","fgColor":"#793A80","visualizer":"default","highlightColor":"#1A7A3E","particleSystem":{"type":""}},"loop":{"count":0,"loopTo":0,"fadeOut":0,"loopFrom":0,"loopPointType":"end"},"energy":{"max":0,"min":0,"average":0},"artist":"Jamphibious","album":"IN","duration":224663,"waveform":"https://lyrely.s3.amazonaws.com/enormoushandsomelyToad/143607f3-3f41-4405-82e9-839b7f9b7736.dat","metadata":{"album":"IN","albumArtist":null,"composer":"Jordan Michael Reed","genre":"VGM","grouping":null,"releaseDate":null,"bpm":null,"isrc":null,"description":"Music from the game IN, created during the Global Game Jam 2022."}},"text":"Introspection","parent":"g7"}]}] const styles = {"bgColor":null,"fgColor":null,"durationText":{"bold":false,"size":"14px","color":"#fff","hidden":false,"allCaps":false},"primaryColor":"#1A7A3E","showWaveform":1,"trackArtSize":"","folderArtSize":"normal","letterSpacing":"0","highlightColor":null,"secondaryColor":"#787878","trackAlbumText":{"bold":false,"size":"12px","color":"#cecece","hidden":false,"allCaps":false},"trackTitleText":{"bold":true,"size":"14px","color":"#fff","hidden":false,"allCaps":false},"folderTitleText":{"bold":true,"size":"20px","color":"#fff","hidden":false,"allCaps":false},"metadataArtSize":"normal","trackArtistText":{"bold":false,"size":"12px","color":"#cecece","hidden":true,"allCaps":false},"trackBackground":null,"showPlayControls":1,"folderBorderColor":"#aaaaaa","metadataLabelText":{"bold":true,"size":"14px","color":"#cecece","hidden":false,"allCaps":false},"metadataValueText":{"bold":false,"size":"16px","color":"#fff","hidden":false,"allCaps":false},"trackDurationText":{"bold":false,"size":"14px","color":"#fff","hidden":false,"allCaps":false},"folderChevronColor":"#ffffff","currentlyPlayingText":{"bold":true,"size":"16px","color":"#fff","hidden":false,"allCaps":false},"showTrackInformation":1,"showTrackProgressBar":0,"trackBackgroundHover":"#111114","trackPlayButtonColor":"#fff","folderDescriptionText":{"bold":false,"size":"12px","color":"#dedede","hidden":false,"allCaps":false}} const isPreview = false function init() { let selector if (!isNaN(Number(id.slice(0, 1)))) { selector = `#\\3${id.slice(0, 1)} ${id.slice(1)}` } else { selector = `#${id}` } const divs = [...document.querySelectorAll(selector)].filter( (element) => !element.dataset.bound, ) let div = divs[0] if (!div) { div = document.createElement('div') // Create a new div let script = document.scripts[document.scripts.length - 1] // A reference to the currently running script script.parentElement.insertBefore(div, script) // Add the newly-created div to the page } new Player(id, data, div, styles) } class Player { constructor(id, data, container, styles) { this.id = id this.data = data this.container = container this.styles = { trackArtSize: 'normal', showWaveform: true, showPlayControls: true, showTrackInformation: true, showTrackProgressBar: false, fontFamily: 'Open Sans', letterSpacing: 0, fgColor: null, bgColor: null, highlightColor: null, primaryColor: '#d63b5c', secondaryColor: '#787878', trackBackground: null, trackBackgroundHover: '#111114', trackPlayButtonColor: '#fff', currentlyPlayingText: { size: '16px', color: '#fff', bold: true, hidden: false, allCaps: false, }, durationText: { size: '14px', color: '#fff', bold: false, hidden: false, allCaps: false, }, trackTitleText: { size: '14px', color: '#fff', bold: true, hidden: false, allCaps: false, }, trackArtistText: { size: '12px', color: '#cecece', bold: false, hidden: true, allCaps: false, }, trackAlbumText: { size: '12px', color: '#cecece', bold: false, hidden: false, allCaps: false, }, trackDurationText: { size: '14px', color: '#fff', bold: false, hidden: false, allCaps: false, }, metadataLabelText: { size: '14px', color: '#cecece', bold: true, hidden: false, allCaps: false, }, metadataValueText: { size: '16px', color: '#fff', bold: false, hidden: false, allCaps: false, }, metadataArtSize: 'normal', folderTitleText: { size: '20px', color: '#fff', bold: true, hidden: false, allCaps: false, }, folderDescriptionText: { size: '12px', color: '#dedede', bold: false, hidden: false, allCaps: false, }, folderBorderColor: '#aaaaaa', folderChevronColor: '#ffffff', folderArtSize: 'normal', ...styles, } this.uuid = 'mp' + id.split('-').pop() this.state = 'paused' this.audioController = 'element' this.tracks = {} this.trackOrder = [] this.groups = {} this.openGroups = [] this.uiDirty = false this.audioContext = undefined this.audioAnalyzer = undefined this.analyzer = undefined this.audioSource = undefined this.source = undefined this.gainNode = undefined this.audioGainNode = undefined this.startTime = 0 this.ellapsedTime = 0 this.pausedTime = 0 this.mousePosition = 0 this.selectedTrack = null this.defaultTrack = true this.loading = false this.showMetadata = false this.waveData = {} this.loadedTracks = {} this.waveCanvas = null this.loopState = localStorage.getItem('mp-loopState') || 'default' // default | infinite | disabled this.loop = { time: undefined, loopTo: undefined, count: 0, } this.tickMS = 27 this.crossfadeState = 'pending' this.crossfadeAmount = 0 this.songLookaheadTrigger = true this.volume = localStorage.getItem('mp-volume') || 0.75 this.defaultTheme = { bgColor: '#666', fgColor: '#5e263a', highlightColor: '#e44967', visualizer: 'default', } container.dataset.bound = 1 this.init() document.dispatchEvent( new CustomEvent('mpOnReady', { detail: { id: this.id, container } }), ) } init() { if (this.isVisible()) { this.insertIntoDom() this.audioElement = this.getElements('Audio')[0] this.waveformElement = this.getElements('WaveformCanvas')[0] this.audioElement.preservesPitch = false this.audioElement.mozPreservesPitch = false this.audioElement.webkitPreservesPitch = false this.bindEventHandlers() this.selectDefaultTrack() this.onVolumeUpdate() this.padGroups() this.particleSystem = null ;(() => { this.tick() })() } else { setTimeout(() => { this.init() }, 250) } } bindEventHandlers() { const tracks = [...this.getElements('TrackNode')] tracks.forEach((track) => { track.addEventListener('click', () => this.onTrackClick(track.dataset.id), ) }) const groups = [...this.getElements('GroupToggle')] groups.forEach((group) => { group.addEventListener('click', () => this.onGroupClick(group.dataset.id), ) }) this.audioElement.addEventListener('loadeddata', (evt) => { this.selectedTrack.data.loading = false this.startParticleSystem() this.uiDirty = true }) this.getElements('PlayButton').forEach((element) => element.addEventListener('click', (evt) => this.onPlayClick(evt)), ) this.getElements('PrevButton').forEach((element) => element.addEventListener('click', () => { const prevTrack = this.getPrevTrack(this.selectedTrack) this.playTrack(prevTrack.id, 0) }), ) this.getElements('NextButton').forEach((element) => element.addEventListener('click', () => { const nextTrack = this.getNextTrack(this.selectedTrack) this.playTrack(nextTrack.id, 0) }), ) this.getElements('WaveformCanvas')[0].addEventListener( 'mousedown', (evt) => { this.onCanvasMouseDown(evt) }, ) this.getElements('WaveformCanvas')[0].addEventListener( 'touchstart', (evt) => { this.onCanvasMouseDown(evt) }, ) this.getElements('WaveformCanvas')[0].addEventListener('mouseup', (evt) => this.onCanvasMouseUp(evt), ) this.getElements('WaveformCanvas')[0].addEventListener( 'touchend', (evt) => this.onCanvasMouseUp(evt), ) this.getElements('WaveformCanvas')[0].addEventListener( 'mouseleave', (evt) => this.onCanvasMouseLeave(evt.offsetX), ) this.getElements('WaveformCanvas')[0].addEventListener( 'touchcancel', (evt) => this.onCanvasMouseLeave(), ) this.getElements('WaveformCanvas')[0].addEventListener( 'mousemove', (evt) => { this.mousePosition = evt.offsetX }, ) this.getElements('WaveformCanvas')[0].addEventListener( 'touchmove', (evt) => { this.mousePosition = evt.touches[0].clientX - evt.target.getBoundingClientRect().left }, ) this.audioElement.addEventListener('ended', () => { this.onTrackEnded() }) const silence = this.getElements('Silence')[0] silence.addEventListener('ended', () => { silence.play() }) this.getElements('LoopButton').forEach((element) => element.addEventListener('click', () => { this.onLoopButtonClick() }), ) this.getElements('MetadataToggle').forEach((element) => element.addEventListener('click', () => { this.showMetadata = !this.showMetadata this.uiDirty = true }), ) this.getElements('VolumeRange')[0].addEventListener('input', (evt) => { this.volume = evt.target.value if (this.audioContext) { let audioVolume = this.volume let gainVolume = this.volume const currentTime = this.audioContext.currentTime if (this.crossfadeAmount > 0) { audioVolume = Math.max(0, 1 - this.crossfadeAmount) * this.volume gainVolume = this.crossfadeAmount * this.volume } if (this.isSafari()) { this.audioElement.volume = audioVolume } if (this.audioGainNode !== undefined) { this.audioGainNode.gain.linearRampToValueAtTime( audioVolume, currentTime + 0.025, ) } if (this.gainNode !== undefined) { this.gainNode.gain.linearRampToValueAtTime( gainVolume, currentTime + 0.025, ) } } this.uiDirty = true }) window.addEventListener('keydown', (evt) => { const tagName = evt?.target?.tagName const badTags = ['BUTTON', 'INPUT', 'TEXTAREA'] if (evt.keyCode === 32 && !badTags.includes(tagName)) { this.onPlayClick() } }) document.addEventListener('mpOnPlay', (evt) => { const { detail } = evt || {} if (detail.id !== this.id && this.isPlaying()) { this.pausePlayback() } }) } onTrackEnded() { if (!this.songLookaheadTrigger || document.visibilityState == 'hidden') { // Song actually ended, wasn't paused const nextTrack = this.getNextTrack(this.selectedTrack) this.playTrack(nextTrack.id, 0) } } onVolumeUpdate() { const volume = this.volume const color = this.styles.secondaryColor const element = this.getElements('VolumeButton')[0] const volumeBar = this.getElements('VolumeBar')[0] if (volume === 0) { element.innerHTML = this.volumeMute(color) } else if (volume < 0.5) { element.innerHTML = this.volumeOne(color) } else { element.innerHTML = this.volumeTwo(color) } volumeBar.style.width = `${volume * 120}px` localStorage.setItem('mp-volume', volume) } onGroupClick(id) { this.groups[id].data.open = !this.groups[id].data.open this.uiDirty = true } onLoopButtonClick() { if (this.loopState === 'default') { this.loopState = 'infinite' } else if (this.loopState === 'infinite') { this.loopState = 'disabled' } else if (this.loopState === 'disabled') { this.loopState = 'default' } localStorage.setItem('mp-loopState', this.loopState) this.uiDirty = true } renderDuration(current, total) { const element = this.getElements('DurationContainer')[0] if (!Number.isNaN(total)) { element.innerHTML = `${renderAsMinutes(current)} / ${renderAsMinutes( total, )}` function renderAsMinutes(seconds) { const mins = Math.floor(seconds / 60) const remaining = Math.floor(seconds - mins * 60) return `${mins > 9 ? mins : '0' + mins}:${ remaining > 9 ? remaining : '0' + remaining }` } } } renderProgressBar(current, total) { const bars = this.getElements('TrackProgressBarAmount') const amount = !Number.isNaN(total) ? (current * 100) / total : 0 const currentTrackId = this.selectedTrack?.id bars.forEach((bar) => { const trackId = bar.dataset.track if (trackId === currentTrackId) { bar.style.width = `${amount}%` } else { bar.style.width = '0%' } }) } onPlayClick() { if (this.isPlaying()) { this.pausePlayback() } else { this.playTrack(this.selectedTrack.id, this.pausedTime) } } pausePlayback() { if (this.isPlaying()) { this.pausedTime = this.getCurrentTime() this.audioContext.suspend() if (this.audioController === 'element') { this.audioElement.pause() } this.state = 'paused' this.stopParticleSystem() this.uiDirty = true } } onCanvasMouseDown() { this.canvasMouseDown = true } onCanvasMouseLeave(offsetX) { this.mousePosition = 0 if (this.canvasMouseDown && offsetX < 0) { this.onCanvasMouseUp() } else if ( this.canvasMouseDown && offsetX > this.waveformElement.clientWidth - 10 ) { const nextTrack = this.getNextTrack(this.selectedTrack) this.playTrack(nextTrack.id, 0) } window.getSelection().removeAllRanges() this.canvasMouseDown = false } onCanvasMouseUp() { if (this.canvasMouseDown) { this.canvasMouseDown = false const element = this.waveformElement const canvasWidth = element.clientWidth const offsetX = this.mousePosition if (offsetX > canvasWidth) { const nextTrack = this.getNextTrack(this.selectedTrack) this.playTrack(nextTrack.id, 0) } else { const duration = this.getDuration() const position = Math.min( Math.max(0, (offsetX / canvasWidth) * duration), duration, ) if (this.audioController === 'element') { this.audioElement.currentTime = position } else { if (this.isPlaying()) { const loopEnd = this.getLoopEnd() if (loopEnd < position) { this.loop.count = 0 } const source = this.createSource({ buffer: this.selectedTrack.data.buffer, loop: this.loop, }) source.start(0, position) this.resetVolumes() this.startTime = this.audioContext.currentTime } } this.ellapsedTime = position this.pausedTime = position } } this.mousePosition = 0 } async onTrackClick(id) { if (this.isPlaying() && this.selectedTrack.id === id) { this.pausePlayback() } else { if (this.selectedTrack.id === id) { this.playTrack(id, this.pausedTime) } else { this.playTrack(id, 0) } } } async playTrack(id, offset) { if (isPreview) { return this.previewTrack(id) } if (!this.isSafari()) { const silence = this.getElements('Silence')[0] silence.play() } document.dispatchEvent( new CustomEvent('mpOnPlay', { detail: { id: this.id } }), ) const currentTime = this.getCurrentTime() const isNewTrack = id !== this.selectedTrack.id || this.defaultTrack this.bindAudioContext() this.state = 'playing' this.defaultTrack = false this.crossfadeAmount = 0 if (this.source) { this.source.stop() this.source.disconnect() } this.audioElement.pause() this.resetVolumes() //reset gain nodes, etc this.uiDirty = true this.songLookaheadTrigger = true if (isNewTrack) { this.selectedTrack = this.tracks[id] const loop = this.selectedTrack?.data?.loop || {} this.loop = { ...loop } this.loadWaveform(id, false) this.addParticleSystem(this.selectedTrack) } this.startTime = this.audioContext.currentTime this.ellapsedTime = offset if (this.tracks[id]?.data?.buffer) { // Track has already been downloaded this.audioController = 'api' const source = this.createSource({ buffer: this.tracks[id].data.buffer, loop: this.loop, }) if (isNewTrack) { source.start() } else { source.start(0, currentTime) } this.onNewAudioAnalyzer(this.analyzer) this.selectedTrack.data.loading = false this.startParticleSystem() } else { // Start audio element, download buffer this.audioController = 'element' this.selectedTrack.data.loading = true if (isNewTrack) { const trackUrlData = await this.getTrackUrl(id) this.audioElement.src = trackUrlData.url if (!this.isSafari()) { setTimeout(() => this.getTrackBuffer(id, trackUrlData.url), 1000) } } if (this.isSafari()) { this.audioElement.volume = this.volume } this.audioElement.play() this.audioElement.currentTime = offset this.onNewAudioAnalyzer(this.audioAnalyzer) } this.audioContext.resume() } resetVolumes() { const cancelTime = Math.max(0, this.audioContext.currentTime - 10) const volume = this.volume if (this.gainNode) { this.gainNode.gain.cancelScheduledValues(cancelTime) this.gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime) } if (this.audioGainNode) { this.audioGainNode.gain.cancelScheduledValues(cancelTime) this.audioGainNode.gain.setValueAtTime( volume, this.audioContext.currentTime, ) } } async previewTrack(id) { this.selectedTrack = this.tracks[id] const loop = this.selectedTrack?.data?.loop || {} this.loop = { ...loop } this.loadWaveform(id, true) this.addParticleSystem(this.selectedTrack) this.uiDirty = true } getTrackBuffer(id, url) { if (!this.tracks[id].data.bufferRequested) { const request = new XMLHttpRequest() request.open('GET', url, true) request.responseType = 'arraybuffer' this.tracks[id].data.bufferRequested = true request.onload = () => { let audioData = request.response this.audioContext.decodeAudioData( audioData, (buffer) => { this.tracks[id].data.buffer = buffer if (this.selectedTrack.id === id && this.state === 'playing') { this.gainNode.gain.value = 0 const source = this.createSource({ buffer, loop: this.loop }) source.start(0, this.getStartOffset()) this.crossfadeState = 'active' } }, (e) => { console.error(`Error with decoding audio data ${e.error}`) }, ) } request.send() } } addParticleSystem(track) { const particleSystem = track.data.theme?.particleSystem?.data if (particleSystem) { this.particleSystem = createParticleSystem({ background: this.getElements('ParticleBackground')[0], foreground: this.getElements('ParticleForeground')[0], data: particleSystem, player: this, waveData: this.waveData[track.id], }) } else { this.stopParticleSystem() this.particleSystem = null } } startParticleSystem() { if (this.particleSystem) { this.particleSystem.start() } } stopParticleSystem() { if (this.particleSystem) { this.particleSystem.stop() } } async getTrackUrl(id) { if (!this.loadedTracks[id] || this.isTrackUrlExpired(id)) { const playResponse = await fetch( `${apiUrl}/player/${this.id}/${id}/play?sessionId=${sessionId}`, ) if (playResponse.ok) { const playData = await playResponse.json() this.loadedTracks[id] = { url: playData.url, expires: Date.now() + playData.expires * 1000, } } } return this.loadedTracks[id] } async loadWaveform(id, defaultTrack) { if (!this.waveData[id]) { this.setLoading(true) const url = this.tracks[id]?.data?.waveform if (url) { const waveformResponse = await fetch(url) if (waveformResponse) { const buffer = await waveformResponse.arrayBuffer() const waveformData = getWaveformData(buffer) const waveData = waveformData.data const tempData = [...waveData] .map((d) => Math.abs(d)) .sort((a, b) => b - a) const max = tempData.slice(0, 100).reduce((acc, cur) => acc + cur, 0) / 100 const min = tempData .slice(tempData.length - 100, tempData.length) .reduce((acc, cur) => acc + cur, 0) / 100 const normalized = [] for (let i = 0; i < waveData.length; i += 2) { const x = waveData[i] const y = waveData[i + 1] normalized.push([x / max, y / max]) } const average = normalized.reduce( (acc, cur) => acc + (Math.abs(cur[0]) + Math.abs(cur[1])) / 2, 0, ) / normalized.length const sumSquares = Math.sqrt( normalized.reduce((acc, cur) => { const amp = Math.max(Math.abs(cur[0]), Math.abs(cur[1])) return acc + amp * amp }, 0) / normalized.length, ) let drawableData = normalized while (drawableData.length > 2500) { drawableData = this.reduceWaveData(drawableData) } const drawableMin = Math.abs( Math.min(...drawableData.map((d) => d[0])), ) const drawableMax = Math.max(...drawableData.map((d) => d[1])) drawableData = drawableData.map((d) => [ d[0] / drawableMin, d[1] / drawableMax, ]) this.waveData[id] = { data: normalized, original: waveData, min: min / max, max, average, drawableData, sumSquares, } } } else { const waveformResponse = await fetch( `${apiUrl}/player/${this.id}/${id}/waveform`, ) if (waveformResponse.ok) { const waveformData = await waveformResponse.json() this.waveData[id] = { data: waveformData, } } else { this.waveData[id] = { error: true, } } } } const canvas = this.getElements('WaveformCanvas')[0] if (this.waveCanvas) { this.waveCanvas.state = 'complete' } this.waveCanvas = new WaveCanvas( canvas, this, this.waveData[id].drawableData, { ...this.defaultTheme, ...(this.selectedTrack?.data?.theme || {}), }, defaultTrack, ) this.onNewAudioAnalyzer(this.audioAnalyzer) } padGroups() { const groups = this.getElements('GroupChildren') const padAmount = 8 const padGroup = (group) => { const tracks = this.getChildElements(group, 'TrackNode') const childGroups = this.getChildElements(group, 'GroupAccordion') const groupsToProcess = this.getChildElements(group, 'GroupNode') tracks.forEach((track) => { const currentPad = parseInt(track.style.marginLeft, 10) || 0 const amount = `${currentPad + padAmount}px` track.style.marginLeft = amount track.style.width = `calc(100% - ${amount})` }) childGroups.forEach((childGroup) => { const currentPad = parseInt(childGroup.style.marginLeft, 10) || 0 const amount = `${currentPad + padAmount}px` childGroup.style.marginLeft = amount childGroup.style.width = `calc(100% - ${amount})` }) groupsToProcess.forEach(padGroup) } groups.forEach(padGroup) } reduceWaveData(data) { const reduced = [] for (let i = 0; i < data.length; i += 2) { if (data[i + 1]) { const x = (Number(data[i][0]) + Number(data[i + 1][0])) / 2 const y = (Number(data[i][1]) + Number(data[i + 1][1])) / 2 reduced.push([x, y]) } else { reduced.push([...data[i]]) } } return reduced } getElements(className) { return [ ...this.container.getElementsByClassName(`${this.uuid}-${className}`), ] } getChildElements(container, className) { return [...container.getElementsByClassName(`${this.uuid}-${className}`)] } updatePlayPause() { const color = this.selectedTrack?.data?.theme?.highlightColor || '#e44967' const elements = this.getElements('PlayButton') elements.forEach((element) => { if (this.isPlaying()) { if (this.selectedTrack?.data?.loading) { element.innerHTML = this.loadingSpinner() } else { element.innerHTML = this.pauseButton(color, 50, 50) } } else { element.innerHTML = this.playButton(color, 50, 50) } }) } isTrackUrlExpired(id) { return Date.now() > this?.loadedTracks[id]?.expires } getNextTrack(currentTrack) { const index = this.trackOrder.findIndex( (child) => child.id === currentTrack.id, ) if (this.trackOrder[index + 1]) { return this.trackOrder[index + 1] } return this.trackOrder[0] } getPrevTrack(currentTrack) { const index = this.trackOrder.findIndex( (child) => child.id === currentTrack.id, ) if (this.trackOrder[index - 1]) { return this.trackOrder[index - 1] } return this.trackOrder[this.trackOrder.length - 1] } getStartOffset() { return ( (this.audioElement.currentTime * 1000 + 27 + this.audioContext.baseLatency * 2) / 1000 ) } selectDefaultTrack() { let trackId = null Object.values(this.tracks).forEach((track) => { if (track?.data?.selected) { trackId = track.id } }) if (!trackId) { trackId = Object.keys(this.tracks)?.[0] } if (trackId) { this.selectedTrack = this.tracks[trackId] this.loadWaveform(trackId, true) this.openParent(this.selectedTrack.parent) this.uiDirty = true } } openParent(parentId) { const group = this.groups[parentId] if (group?.data) { group.data.open = true } if (group?.parent) { this.openParent(group.parent) } } getCurrentTime() { if (this.audioController === 'element') { return this.audioElement.currentTime } else if (this.audioController === 'api') { const currentTime = this.audioContext.currentTime return this.ellapsedTime + currentTime - this.startTime } return 0 } getDuration() { const duration = this?.selectedTrack?.data?.duration || 0 return duration / 1000 } isPlaying() { return this.state === 'playing' } isVisible() { const el = this.container return Boolean( el && (el.offsetParent || el.offsetWidth || el.offsetHeight), ) } tick() { if (!this.isVisible()) { //this.pausePlayback() } this.handleUiUpdate() this.handleAudioBufferTransition() // Handles removing loop if song should end, this way onended will fire if (this.songLookaheadTrigger) { const currentTime = this.getCurrentTime() const duration = this.getDuration() if (currentTime + 2 > duration) { if ( this.loop.count === 0 && this.loop.fadeOut && this.loopState !== 'infinite' ) { this.loop.fadeOut = false this.gainNode.gain.setValueAtTime( this.gainNode.gain.value, this.audioContext.currentTime, ) this.gainNode.gain.linearRampToValueAtTime( 0, this.audioContext.currentTime + duration - currentTime, ) } } const end = this.source?.loop ? this.source.loopEnd : duration if (currentTime + 0.2 > end) { // If looping, check if inf, if not subtract and remove loop if required. if ( this.loopState === 'infinite' || (this.loop?.count > 0 && this.loopState === 'default') ) { this.ellapsedTime = this.getLoopStart(this.loop) this.startTime = this.audioContext.currentTime if (this.loopState === 'default') { this.loop.count -= 1 } } else { this.songLookaheadTrigger = false if (this.source) { this.source.loop = false } } } } setTimeout(() => { this.tick() }, this.tickMS) } getLoopStart(loop) { return Number(loop.loopTo) } getLoopEnd() { return this.loop?.loopPointType === 'custom' ? Number(this.loop.loopFrom) : this.getDuration() } handleUiUpdate() { const currentTime = this.getCurrentTime() const duration = this.getDuration() let currentDuration = currentTime if (this.canvasMouseDown) { const canvasWidth = this.waveformElement.clientWidth const offsetX = this.mousePosition currentDuration = Math.min( Math.max(0, (offsetX / canvasWidth) * duration), duration, ) } this.renderDuration(currentDuration, duration) this.renderProgressBar(currentDuration, duration) const container = this.getElements('PlayerContainer')[0] if (container.clientWidth < 600) { container.classList.remove('desktop') } else { container.classList.add('desktop') } //ui-dirty if (this.uiDirty) { this.updateTracks() this.updateGroups() this.generateSelectedTrackStyle() this.onVolumeUpdate() this.updatePlayPause() this.updateLoopButton() this.updatePrevNextButtons() this.updateMetadataToggle() this.renderMetadata(this.showMetadata, this.selectedTrack) this.uiDirty = false } } updatePrevNextButtons() { const color = this.selectedTrack?.data?.theme?.highlightColor || '#e44967' this.getElements('PrevButton').forEach( (element) => (element.innerHTML = this.prevIcon(color)), ) this.getElements('NextButton').forEach( (element) => (element.innerHTML = this.nextIcon(color)), ) } updateGroups() { const groupElements = [...this.getElements('GroupNode')] const className = `${this.uuid}-Open` groupElements.forEach((groupElement) => { const id = groupElement.dataset.id if (this.groups[id].data.open) { groupElement.classList.add(className) } else { groupElement.classList.remove(className) } }) } updateTracks() { const title = this.selectedTrack.text this.getElements('NameContainer')[0].innerHTML = `${title}` const trackElements = [...this.getElements('TrackNode')] const selectedClassName = `${this.uuid}-Selected` const playingClassName = `${this.uuid}-Playing` trackElements.forEach((trackElement) => { if (trackElement.dataset.id === this.selectedTrack.id) { trackElement.classList.add(selectedClassName) if (this.isPlaying()) { trackElement.classList.add(playingClassName) } else { trackElement.classList.remove(playingClassName) } } else { trackElement.classList.remove(selectedClassName, playingClassName) } }) } onNewAudioAnalyzer(analyzer) { if (this.waveCanvas) { this.waveCanvas.setAudioAnalyzer(analyzer) } } handleAudioBufferTransition() { const { currentTime } = this?.audioContext || {} if (this.crossfadeState === 'active') { const delta = 0.5 this.crossfadeAmount = Math.min(1, this.crossfadeAmount + delta) if (this.crossfadeAmount >= 1) { this.crossfadeState = 'pending' this.ellapsedTime = this.audioElement.currentTime this.audioElement.pause() this.onNewAudioAnalyzer(this.analyzer) this.startTime = this.audioContext.currentTime this.audioController = 'api' } const audioVolume = Math.max(0, 1 - this.crossfadeAmount) * this.volume const gainVolume = this.crossfadeAmount * this.volume if (this.audioGainNode !== undefined) { this.audioGainNode.gain.linearRampToValueAtTime( audioVolume, currentTime + this.tickMS / 1000, ) } if (this.gainNode !== undefined) { this.gainNode.gain.linearRampToValueAtTime( gainVolume, currentTime + this.tickMS / 1000, ) } } } updateLoopButton() { const color = this.styles.primaryColor const disabledColor = this.styles.secondaryColor const loopButtons = this.getElements('LoopButton') loopButtons.forEach((loopButton) => { if (this.loopState === 'default') { loopButton.innerHTML = this.loopIcon(color) } else if (this.loopState === 'infinite') { loopButton.innerHTML = this.infLoopIcon(color) } else if (this.loopState === 'disabled') { loopButton.innerHTML = this.loopIcon(disabledColor) } }) } updateMetadataToggle() { const color = this.styles.primaryColor const disabledColor = this.styles.secondaryColor const elements = this.getElements('MetadataToggle') elements.forEach((element) => { if (this.showMetadata) { element.innerHTML = this.pageIcon(color) } else { element.innerHTML = this.pageIcon(disabledColor) } }) } bindAudioContext() { if (!this.audioContext) { this.audioContext = new AudioContext() this.analyzer = this.audioContext.createAnalyser() this.analyzer.fftSize = 4096 if (!this.isSafari()) { this.audioAnalyzer = this.audioContext.createAnalyser() this.audioAnalyzer.fftSize = 4096 this.audioSource = this.audioContext.createMediaElementSource( this.audioElement, ) this.audioSource.connect(this.audioAnalyzer) this.audioGainNode = this.audioContext.createGain() this.audioGainNode.gain.value = 1 this.audioAnalyzer.connect(this.audioGainNode) this.audioGainNode.connect(this.audioContext.destination) } this.gainNode = this.audioContext.createGain() this.gainNode.gain.value = 0 this.analyzer.connect(this.gainNode) this.gainNode.connect(this.audioContext.destination) this.audioContext.resume() } } createSource({ buffer, loop }) { if (this.source) { this.source.stop() this.source.disconnect() } this.source = new AudioBufferSourceNode(this.audioContext) this.source.connect(this.analyzer) this.source.addEventListener('ended', () => { this.onTrackEnded() }) this.source.buffer = this.cloneAudioBuffer(buffer) if (loop?.count > 0) { this.source.loop = true this.source.loopStart = this.getLoopStart(loop) this.source.loopEnd = this.getLoopEnd() } else { this.source.loop = false } return this.source } isSafari() { const userAgent = navigator.userAgent const chrome = userAgent.indexOf('Chrome') > -1 const safari = userAgent.indexOf('Safari') > -1 return safari && !chrome } cloneAudioBuffer(fromAudioBuffer) { const audioBuffer = new AudioBuffer({ length: fromAudioBuffer.length, numberOfChannels: fromAudioBuffer.numberOfChannels, sampleRate: fromAudioBuffer.sampleRate, }) for ( let channelI = 0; channelI < audioBuffer.numberOfChannels; ++channelI ) { const samples = fromAudioBuffer.getChannelData(channelI) audioBuffer.copyToChannel(samples, channelI) } return audioBuffer } insertIntoDom() { const style = (name) => `${this.uuid}-${name}` const trackArtSizes = { '': '48px', small: '48px', normal: '64px', large: '84px', } const metadataArtDimensions = { small: '150px', normal: '200px', large: '250px', } const folderArtDimensions = { small: '48px', normal: '60px', large: '72px', } const waveformAreaVisible = this.styles.showWaveform || this.styles.showPlayControls || this.styles.showTrackInformation const trackNodeSize = trackArtSizes[this.styles.trackArtSize] const volumeHoverKey = `VolumeContainer:hover .${style( 'VolumeRangeContainer', )}` const uuid = this.uuid const TrackNodePlusGroupNode = `TrackNode + .${uuid}-GroupNode` const OpenGroupChildren = `GroupNode.${uuid}-Open > .${uuid}-GroupChildren` const OpenGroupChevron = `GroupNode.${uuid}-Open > .${uuid}-GroupAccordion .${uuid}-Chevron:before` const GroupChildTracks = `GroupNode .${uuid}-TrackNode` const GroupContentWithArt = `GroupArt + .${uuid}-GroupContent` const TrackPlayStateHover = `TrackNode:hover .${uuid}-TrackPlayState` const TrackNodePlayingPlayState = `TrackNode.${uuid}-Playing .${uuid}-TrackPlayState` const SelectedTrackTitle = `TrackNode.${uuid}-Selected .${uuid}-TrackTitle` const { currentlyPlayingText, durationText, trackTitleText, trackAlbumText, trackArtistText, trackDurationText, trackArtSize, trackBackground, trackBackgroundHover, primaryColor, secondaryColor, showTrackInformation, metadataLabelText, metadataValueText, metadataArtSize, folderTitleText, folderDescriptionText, folderBorderColor, folderChevronColor, folderArtSize, } = this.styles const showControlsContainer = showTrackInformation || !currentlyPlayingText?.hidden || !durationText?.hidden const largerTrackArtSize = trackArtSize === 'normal' || trackArtSize === 'large' const styles = { mobile: { PlayerContainer: ` `, WaveContainer: ` display: flex; flex-direction: column-reverse; position: relative; justify-content: center; `, PlayContainerMobile: ` display: ${this.styles.showPlayControls ? 'flex' : 'none'}; justify-content: center; align-items: center; margin: 16px 0; `, 'PlayContainerMobile > button': ` padding: 0; width: 50px; height: 50px; display: flex; justify-content: center; align-items: center; margin-bottom: 8px; `, PlayContainerDesktop: ` display: none; justify-content: center; align-items: center; padding-right: 1em; `, PlayButton: ` border: 0; background: transparent; border-radius: 500em; padding: 10px; transition: 0.2s ease-out; `, 'PlayButton:hover': ` filter: brightness(125%) contrast(75%); `, 'PlayButton:active': ` filter: brightness(75%); `, PrevButton: ` border: 0; background: transparent; border-radius: 500em; padding: 10px 5px; transition: 0.2s ease-out; `, 'PrevButton:hover': ` filter: brightness(125%) contrast(75%); `, 'PrevButton:active': ` filter: brightness(75%); `, NextButton: ` border: 0; background: transparent; border-radius: 500em; padding: 10px 5px; transition: 0.2s ease-out; `, 'NextButton:hover': ` filter: brightness(125%) contrast(75%); `, 'NextButton:active': ` filter: brightness(75%); `, Waveform: ` display: ${this.styles.showWaveform ? 'block' : 'none'}; flex: 1; `, WaveformCanvasContainer: ` position: relative; height: 150px; `, ParticleContainer: ` position: absolute; pointer-events: none; top: -50px; width: 100%; height: 205px; display: ${this.styles.showWaveform ? 'block' : 'none'}; `, ParticleBackground: ` display: block; position: absolute; `, ParticleForeground: ` display: block; position: absolute; `, WaveformCanvas: ` position: absolute; left: 0; top: 0; `, Audio: ` display: none; `, ControlsContainer: ` display: ${showControlsContainer ? 'flex' : 'none'}; flex-wrap: wrap; padding: ${this.styles.showWaveform ? '8px' : '0px'} 8px 0 8px; position: relative; `, NameContainer: ` font-size: ${currentlyPlayingText.size}; color: ${currentlyPlayingText.color}; font-weight: ${currentlyPlayingText.bold ? 'bold' : 'normal'}; text-transform: ${ currentlyPlayingText.allCaps ? 'uppercase' : 'none' }; display: block; opacity: ${currentlyPlayingText.hidden ? '0' : '1'}; padding: 12px 0 10px; flex: 1; text-align: center; `, FlexSpacer: ` flex: 0; `, MetadataToggleContainer: ` display: ${showTrackInformation ? 'flex' : 'none'}; align-items: center; margin-right: 12px; `, MetadataToggle: ` padding: 0; border: 0; background: transparent; `, VolDurContainer: ` display: none; flex-direction: row; justify-content: flex-end; `, DurationContainer: ` font-size: ${durationText.size}; color: ${durationText.color}; font-weight: ${durationText.bold ? 'bold' : 'normal'}; text-transform: ${durationText.allCaps ? 'uppercase' : 'none'}; display: ${durationText.hidden ? 'none' : 'flex'}; align-items: center; `, VolumeContainer: ` display: ${showTrackInformation ? 'flex' : 'none'}; align-items: center; `, VolumeRangeContainer: ` display: none; position: relative; height: 16px; align-items: center; font-family: Arial; font-size: 16px; line-height: 1; `, [volumeHoverKey]: ` display: flex; `, VolumeButton: ` background: transparent; border: 0; padding: 0 8px; `, LoopContainer: ` display: ${showTrackInformation ? 'flex' : 'none'}; align-items: center; margin-right: 6px; `, LoopButton: ` background: transparent; border: 0; padding: 0 8px; `, VolumeBar: ` position: absolute; left: 0px; height: 12px; top: 2px; border-radius: 100em; pointer-events: none; min-width: 20px; `, GroupAccordion: ` padding: 4px 0; margin-bottom: 8px; ${ !!folderBorderColor ? `border-bottom: 1px solid ${folderBorderColor};` : '' } `, GroupArt: ` `, 'GroupArt img': ` width: ${folderArtDimensions[folderArtSize]}; height: ${folderArtDimensions[folderArtSize]}; object-fit: cover; margin-right: 16px; `, GroupContent: ` display: flex; flex: 1; flex-direction: column; padding-right: 0.75em; justify-content: center; `, [GroupContentWithArt]: ` padding-top: 4px; `, GroupTitle: ` font-size: ${folderTitleText.size}; color: ${folderTitleText.color}; font-weight: ${folderTitleText.bold ? 'bold' : 'normal'}; text-transform: ${folderTitleText.allCaps ? 'uppercase' : 'none'}; display: ${folderTitleText.hidden ? 'none' : 'block'}; margin-bottom: 0.25em; `, LibraryContainer: ` display: flex; flex-direction: column; `, TracksContainer: ` flex: 1; padding: 0 4px 0 8px; margin-top: 12px; `, MetadataContainer: ` flex: 0; display: none; margin-top: 16px; padding: 16px; position: relative; `, 'MetadataContainer.open': ` display: flex; flex: 1; flex-wrap: wrap; `, MetadataCloseContainer: ` position: absolute; top: -16px; right: 0; `, MetadataCloseButton: ` background: transparent; padding: 16px; border: 0; `, MetadataAlbumArt: ` width: 100%; display: flex; align-items: center; justify-content: center; margin-bottom: 32px; `, MetadataItem: ` text-align: left; min-height: 55px; padding: 0 8px; `, MetadataItemDescription: ` text-align: left; min-height: 55px; padding: 0 8px; width: 600px; max-width: 100%; `, MetadataLabel: ` font-size: ${metadataLabelText.size}; color: ${metadataLabelText.color}; font-weight: ${metadataLabelText.bold ? 'bold' : 'normal'}; text-transform: ${metadataLabelText.allCaps ? 'uppercase' : 'none'}; display: ${metadataLabelText.hidden ? 'none' : 'block'}; `, MetadataValue: ` font-size: ${metadataValueText.size}; color: ${metadataValueText.color}; font-weight: ${metadataValueText.bold ? 'bold' : 'normal'}; text-transform: ${metadataValueText.allCaps ? 'uppercase' : 'none'}; display: ${metadataValueText.hidden ? 'none' : 'block'}; margin-bottom: 20px; `, AlbumArt: ` width: ${metadataArtDimensions[metadataArtSize]}; height: ${metadataArtDimensions[metadataArtSize]}; display: ${metadataArtSize === '' ? 'none' : 'block'}; `, Hide: ` display: none; `, [TrackNodePlusGroupNode]: ` margin-top: 8px; `, GroupChildren: ` padding-bottom: 8px; display: none; `, [OpenGroupChildren]: ` display: block; `, TrackNode: ` display: flex; background: none; border: 0; cursor: pointer; align-items: center; text-align: left; padding: 0 16px 0 0; transition: 0.2s ease-out; width: 100%; position: relative; filter: none; align-items: stretch; ${!!trackArtSize ? 'margin-bottom: 4px;' : ''} ${trackBackground ? `background: ${trackBackground};` : ''} `, 'TrackNode:hover': ` background: ${trackBackgroundHover}; `, [GroupChildTracks]: ` `, TrackNumber: ` margin-right: 1em; font-size: 16px; font-weight: bold; line-height: 15px; display: none; `, TrackArtContainer: ` width: 40px; height: 40px; margin-right: 1rem; background: rgba(80,80,80,0.1); `, TrackArt: ` width: 100%; `, TrackDescription: ` flex: 1; position: relative; display: flex; flex-direction: column; justify-content: center; min-height: 58px; `, TrackTitle: ` font-size: ${trackTitleText.size}; color: ${trackTitleText.color}; font-weight: ${trackTitleText.bold ? 'bold' : 'normal'}; text-transform: ${trackTitleText.allCaps ? 'uppercase' : 'none'}; display: ${trackTitleText.hidden ? 'none' : 'block'}; transition: 0.2s ease-out; `, [SelectedTrackTitle]: ` color: ${primaryColor}; `, TrackArtist: ` font-size: ${trackArtistText.size}; color: ${trackArtistText.color}; font-weight: ${trackArtistText.bold ? 'bold' : 'normal'}; text-transform: ${trackArtistText.allCaps ? 'uppercase' : 'none'}; display: ${trackArtistText.hidden ? 'none' : 'block'}; `, TrackAlbum: ` font-size: ${trackAlbumText.size}; color: ${trackAlbumText.color}; font-weight: ${trackAlbumText.bold ? 'bold' : 'normal'}; text-transform: ${trackAlbumText.allCaps ? 'uppercase' : 'none'}; display: ${trackAlbumText.hidden ? 'none' : 'block'}; `, TrackDuration: ` margin-left: 1rem; font-size: ${trackDurationText.size}; color: ${trackDurationText.color}; font-weight: ${trackDurationText.bold ? 'bold' : 'normal'}; text-transform: ${trackDurationText.allCaps ? 'uppercase' : 'none'}; display: ${trackDurationText.hidden ? 'none' : 'flex'}; align-items: center; `, TrackProgressBar: ` height: 2px; width: calc(100% + 46px); position: absolute; bottom: ${largerTrackArtSize ? '8px' : '2px'}; background: ${secondaryColor}; `, TrackProgressBarAmount: ` position: absolute; left: 0; top: 0; height: 2px; max-width: 100%; `, TrackPlayState: ` transition: 0.2s ease-out; margin-right: ${trackArtSize === '' ? '0px' : '16px'}; height: ${trackNodeSize}; width: ${trackNodeSize}; display: flex; align-items: center; justify-content: center; background-repeat: no-repeat; background-size: cover; align-self: center; `, 'TrackPlayState .PlayIcon': ` display: flex; border-radius: 500em; width: 29px; height: 30px; padding-left: 1px; justify-content: center; align-items: center; `, 'TrackPlayState.hasArt .PlayIcon': ` background: #00000088; `, 'TrackPlayState .PauseIcon': ` display: none; `, [TrackPlayStateHover]: ` `, [`${TrackNodePlayingPlayState} .PlayIcon`]: ` display: none; `, [`${TrackNodePlayingPlayState} .PauseIcon`]: ` display: inline-block; `, GroupToggle: ` border: 0; text-align: left; padding: 0; background: transparent; cursor: pointer; width: 100%; display: flex; flex-direction: row; align-items: stretch; `, GroupDescription: ` font-size: ${folderDescriptionText.size}; color: ${folderDescriptionText.color}; font-weight: ${folderDescriptionText.bold ? 'bold' : 'normal'}; text-transform: ${ folderDescriptionText.allCaps ? 'uppercase' : 'none' }; display: ${folderDescriptionText.hidden ? 'none' : 'block'}; `, ChevronContainer: ` display: flex; align-items: center; justify-content: center; width: 40px; `, Chevron: ` line-height: 1; color: ${folderChevronColor}; `, 'Chevron:before': ` font-size: 24px; border-style: solid; border-width: 0.1em 0.1em 0 0; content: ''; display: inline-block; height: 0.35em; left: 0.15em; position: relative; top: 0.15em; transform: rotate(-45deg); vertical-align: top; width: 0.35em; `, [OpenGroupChevron]: ` top: 0; transform: rotate(135deg); `, }, desktop: { PlayerContainer: ` padding: ${waveformAreaVisible ? '16px' : ''} 0; `, WaveContainer: ` flex-direction: row; `, ParticleContainer: ` height: 250px; `, PlayContainerDesktop: ` display: ${this.styles.showPlayControls ? 'flex' : 'none'}; `, PlayContainerMobile: ` display: none; `, LibraryContainer: ` `, MetadataFirstRow: ` flex-direction: row; `, FirstRowLeftSide: ` padding-bottom: 0px; `, AlbumArt: ` margin-right: 16px; `, FirstRowRightSide: ` flex-direction: row; `, MetadataSecondRow: ` flex-direction: row; padding: 32px 0 16px; `, NameContainer: ` text-align: left; align-items: center; padding: 2px 0 0 0; `, FlexSpacer: ` flex: 1; `, VolDurContainer: ` display: flex; `, MetadataContainer: ` flex: 0; display: none; margin-top: 16px; padding: 16px; position: relative; `, 'MetadataContainer.open': ` display: block; flex: 1; `, MetadataAlbumArt: ` float: left; display: block; width: auto; margin: 0; `, MetadataItem: ` text-align: left; min-height: 55px; padding: 0 8px; float: left; width: 200px; `, MetadataItemDescription: ` text-align: left; min-height: 55px; padding: 0 8px; float: left; width: 600px; max-width: 100%; `, }, } let trackNumber = 1 const renderNode = (node) => { const firstChar = node.id.slice(0, 1) let children = '' if (node?.children?.length > 0) { children = `