Custom Audio Player

This commit is contained in:
Evert Prants 2018-10-05 16:28:44 +03:00
parent 28732c0ea3
commit b8d4cf6fa1
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
12 changed files with 379 additions and 7 deletions

View File

@ -34,8 +34,7 @@ function filewalker (dir, done) {
// If directory, execute a recursive call // If directory, execute a recursive call
if (stat && stat.isDirectory()) { if (stat && stat.isDirectory()) {
// Add directory to array [comment if you need to remove the directories from the array] //results.push(file)
results.push(file)
filewalker(file, function (err, res) { filewalker(file, function (err, res) {
if (err) return done(err) if (err) return done(err)

10
public/icon/pause.svg Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 438.536 438.536" style="enable-background:new 0 0 438.536 438.536;" xml:space="preserve">
<g>
<g>
<path d="M164.453,0H18.276C13.324,0,9.041,1.807,5.425,5.424C1.808,9.04,0.001,13.322,0.001,18.271v401.991 c0,4.948,1.807,9.233,5.424,12.847c3.619,3.617,7.902,5.428,12.851,5.428h146.181c4.949,0,9.231-1.811,12.847-5.428 c3.617-3.613,5.424-7.898,5.424-12.847V18.271c0-4.952-1.807-9.231-5.428-12.847C173.685,1.807,169.402,0,164.453,0z" fill="#FFFFFF"/>
<path d="M433.113,5.424C429.496,1.807,425.215,0,420.267,0H274.086c-4.949,0-9.237,1.807-12.847,5.424 c-3.621,3.615-5.432,7.898-5.432,12.847v401.991c0,4.948,1.811,9.233,5.432,12.847c3.609,3.617,7.897,5.428,12.847,5.428h146.181 c4.948,0,9.229-1.811,12.847-5.428c3.614-3.613,5.421-7.898,5.421-12.847V18.271C438.534,13.319,436.73,9.04,433.113,5.424z" fill="#FFFFFF"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 306 306" style="enable-background:new 0 0 306 306;" xml:space="preserve">
<g>
<g id="skip-next">
<path d="M0,306l216.75-153L0,0V306z M255,0v306h51V0H255z" fill="#FFFFFF"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 503 B

10
public/icon/play-prev.svg Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 306 306" style="enable-background:new 0 0 306 306;" xml:space="preserve">
<g>
<g id="skip-previous">
<rect width="51" height="306" fill="#FFFFFF"/>
<polygon points="89.25,153 306,306 306,0 " fill="#FFFFFF"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 543 B

3
public/icon/play.svg Normal file
View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 41.999 41.999" style="enable-background:new 0 0 41.999 41.999;" xml:space="preserve" width="512px" height="512px">
<path d="M36.068,20.176l-29-20C6.761-0.035,6.363-0.057,6.035,0.114C5.706,0.287,5.5,0.627,5.5,0.999v40 c0,0.372,0.206,0.713,0.535,0.886c0.146,0.076,0.306,0.114,0.465,0.114c0.199,0,0.397-0.06,0.568-0.177l29-20 c0.271-0.187,0.432-0.494,0.432-0.823S36.338,20.363,36.068,20.176z" fill="#FFFFFF"/></svg>

After

Width:  |  Height:  |  Size: 597 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 347.182 347.182" style="enable-background:new 0 0 347.182 347.182;" xml:space="preserve">
<g>
<path d="M277.808,5.426C274.187,1.809,269.902,0,264.954,0c-4.945,0-9.233,1.809-12.847,5.426L157.034,100.5H82.233 c-4.952,0-9.235,1.809-12.851,5.424c-3.617,3.621-5.426,7.901-5.426,12.85v109.634c0,4.948,1.809,9.232,5.426,12.847 c3.619,3.617,7.902,5.428,12.851,5.428h74.798l95.07,95.077c3.62,3.61,7.904,5.421,12.847,5.421c4.955,0,9.236-1.811,12.854-5.421 c3.613-3.617,5.424-7.901,5.424-12.847V18.276C283.225,13.328,281.421,9.042,277.808,5.426z" fill="#FFFFFF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 882 B

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 461.55 461.55" style="enable-background:new 0 0 461.55 461.55;" xml:space="preserve">
<g>
<g id="volume-off">
<path d="M345.525,229.5c0-45.9-25.5-84.15-63.75-102v56.1l63.75,63.75C345.525,239.7,345.525,234.6,345.525,229.5z M409.275,229.5 c0,22.95-5.1,45.9-12.75,66.3l38.25,38.25c17.85-30.6,25.5-68.85,25.5-107.1c0-109.65-76.5-201.45-178.5-224.4V56.1 C355.725,81.6,409.275,147.9,409.275,229.5z M34.425,0L1.275,33.15L121.125,153H1.275v153h102l127.5,127.5V262.65L340.425,372.3 c-17.851,12.75-35.7,22.95-58.65,30.601v53.55c35.7-7.65,66.3-22.95,94.35-45.9l51,51l33.15-33.149l-229.5-229.5L34.425,0z M230.775,25.5l-53.55,53.55l53.55,53.55V25.5z" fill="#FFFFFF"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

9
public/icon/volume.svg Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 459 459" style="enable-background:new 0 0 459 459;" xml:space="preserve">
<g>
<g id="volume-up">
<path d="M0,153v153h102l127.5,127.5v-408L102,153H0z M344.25,229.5c0-45.9-25.5-84.15-63.75-102v204 C318.75,313.65,344.25,275.4,344.25,229.5z M280.5,5.1v53.55C354.45,81.6,408,147.899,408,229.5S354.45,377.4,280.5,400.35V453.9 C382.5,430.949,459,339.15,459,229.5C459,119.85,382.5,28.049,280.5,5.1z" fill="#FFFFFF"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 746 B

View File

@ -73,9 +73,84 @@ tr:nth-child(even) {
background-color: #112635; background-color: #112635;
cursor: pointer; cursor: pointer;
} }
.paging.btn.inner {
background-color: #102331;
}
.paging.bg { .paging.bg {
background-color: #0e202c; background-color: #0e202c;
} }
.player-controls {
display: flex;
flex-direction: row;
height: 40px;
background-color: #0c2233;
}
.player-controls .grow {
flex-grow: 1;
}
.player-controls span {
display: block;
}
.player-controls .icon {
width: 30px;
height: 30px;
cursor: pointer;
background-size: 60%;
background-repeat: no-repeat;
background-position: center;
margin: 5px;
padding: 0 5px;
}
.player-controls .timestamp {
margin-top: 0.9em;
font-size: 0.8em;
padding: 0 5px;
min-width: 8em;
text-align: center;
}
.play-btn {
background-image: url('icon/play.svg');
}
.pause-btn {
background-image: url('icon/pause.svg');
}
.mute-btn {
background-image: url('icon/volume.svg');
}
.mute1-btn {
background-image: url('icon/volume-low.svg');
}
.muted-btn {
background-image: url('icon/volume-off.svg');
}
.next-btn {
background-image: url('icon/play-next.svg');
}
.prev-btn {
background-image: url('icon/play-prev.svg');
}
.volume {
display: flex;
flex-direction: row;
}
.volume .volume-bar {
width: 5em;
}
.volume .volume-bar .seek-inner {
width: 100%;
}
.seek-container {
height: 8px;
width: 100%;
margin: 16px 5px 0 5px;
background-color: #00131b;
cursor: pointer;
}
.seek-inner {
height: 100%;
width: 0%;
background-color: #00b7ff;
}
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
tr td:nth-child(1), th:nth-child(1) { tr td:nth-child(1), th:nth-child(1) {
display: none; display: none;
@ -100,4 +175,7 @@ tr:nth-child(even) {
.allowance { .allowance {
margin-bottom: 100px !important; margin-bottom: 100px !important;
} }
.volume .volume-bar {
display: none;
}
} }

View File

@ -25,19 +25,32 @@
</div> </div>
<div class="pages flex-row"> <div class="pages flex-row">
<div class="paging btn" id="jump-first" onclick="showTracks(1)"><<</div> <div class="paging btn" id="jump-first" onclick="showTracks(1)"><<</div>
<div class="paging btn" id="jump-prev" onclick="showTracks(pageNum - 1)"><</div> <div class="paging btn inner" id="jump-prev" onclick="showTracks(pageNum - 1)"><</div>
<div class="paging bg" id="pagenum">0 / 0</div> <div class="paging bg" id="pagenum">0 / 0</div>
<div class="paging btn" id="jump-next" onclick="showTracks(pageNum + 1)">></div> <div class="paging btn inner" id="jump-next" onclick="showTracks(pageNum + 1)">></div>
<div class="paging btn" id="jump-last" onclick="showTracks(pages)">>></div> <div class="paging btn" id="jump-last" onclick="showTracks(pages)">>></div>
</div> </div>
<div class="player flex-row"> <div class="player flex-row">
<div id="playing">Nothing playing</div> <div id="playing">Nothing playing</div>
<audio id="player"></audio> <audio id="player"></audio>
<div class="player-controls"> <div class="player-controls">
<span class="play-btn "> <span class="prev-btn flex-col icon" id="player-prev"></span>
<span class="play-btn flex-col icon" id="player-play"></span>
<span class="next-btn flex-col icon" id="player-next"></span>
<span class="timestamp flex-col" id="player-ts">0:00 / 0:00</span>
<div class="seek-bar seek-container flex-col grow" id="player-seekbar">
<div class="seek-inner"></div>
</div>
<div class="volume flex-col">
<span class="mute-btn flex-col icon" id="player-mute"></span>
<div class="volume-bar seek-container flex-col" id="player-volbar">
<div class="seek-inner"></div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<script type="text/javascript" src="index.js"></script> <script type="text/javascript" src="index.js"></script>
<script type="text/javascript" src="player.js"></script>
</body> </body>
</html> </html>

View File

@ -3,6 +3,9 @@ var input = document.querySelector('#search')
var audio = document.querySelector('#player') var audio = document.querySelector('#player')
var playing = document.querySelector('#playing') var playing = document.querySelector('#playing')
var next = document.querySelector('#player-next')
var prev = document.querySelector('#player-prev')
var tableHead = '<tr> \ var tableHead = '<tr> \
<th class="small">#</th> \ <th class="small">#</th> \
<th>Track</th> \ <th>Track</th> \
@ -60,7 +63,6 @@ function httpGet (url, callback) {
} }
function shortTitle (title, artist) { function shortTitle (title, artist) {
title = title.replace(/&amp;/g, '&')
if (!artist) return title if (!artist) return title
return artist + ' - ' + title return artist + ' - ' + title
} }
@ -118,6 +120,7 @@ function search (query) {
} }
function play (id) { function play (id) {
if (id < 1) return
httpGet('/api/track/' + id).then(function (data) { httpGet('/api/track/' + id).then(function (data) {
let title = shortTitle(data.title, data.artist) let title = shortTitle(data.title, data.artist)
playing.innerHTML = title playing.innerHTML = title
@ -140,6 +143,14 @@ audio.addEventListener('ended', function (e) {
play(nowPlaying + 1) play(nowPlaying + 1)
}) })
next.addEventListener('click', function (e) {
play(nowPlaying + 1)
})
prev.addEventListener('click', function (e) {
play(nowPlaying - 1)
})
showTracks(1) showTracks(1)
window.play = play window.play = play

183
public/player.js Normal file
View File

@ -0,0 +1,183 @@
(function () {
var audio = document.querySelector('#player')
var play = document.querySelector('#player-play')
var ts = document.querySelector('#player-ts')
var seek = document.querySelector('#player-seekbar')
var seekBar = document.querySelector('#player-seekbar .seek-inner')
var mute = document.querySelector('#player-mute')
var vol = document.querySelector('#player-volbar')
var volBar = document.querySelector('#player-volbar .seek-inner')
// Seconds into HH:MM:SS
function toHHMMSS (numbr) {
let secNum = parseInt(numbr, 10) // don't forget the second param
let hours = Math.floor(secNum / 3600)
let minutes = Math.floor((secNum - (hours * 3600)) / 60)
let seconds = secNum - (hours * 3600) - (minutes * 60)
if (hours < 10) hours = '0' + hours
if (minutes < 10) minutes = '0' + minutes
if (seconds < 10) seconds = '0' + seconds
let time = ''
if (parseInt(hours) > 0) {
time = hours + ':' + minutes + ':' + seconds
} else {
time = minutes + ':' + seconds
}
return time
}
function relMouseCoords (e) {
let x
let y
if (e.changedTouches) {
let touch = e.changedTouches[0]
if (touch) {
e.pageX = touch.pageX
e.pageY = touch.pageY
}
}
if (e.pageX || e.pageY) {
x = e.pageX
y = e.pageY
} else {
x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft
y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop
}
x -= this.offsetLeft
y -= this.offsetTop
return {x: x, y: y}
}
function updateSeeker() {
if (isNaN(audio.duration) || audio.duration === 0) return
ts.innerHTML = toHHMMSS(audio.currentTime) + ' / ' + toHHMMSS(audio.duration)
let progress = 100 * audio.currentTime / audio.duration
seekBar.style.width = progress + '%'
}
function updateVolume() {
volBar.style.width = (100 * audio.volume) + '%'
if (audio.muted) {
mute.className = 'muted-btn flex-col icon'
} else if (audio.volume > 0.5) {
mute.className = 'mute-btn flex-col icon'
} else {
mute.className = 'mute1-btn flex-col icon'
}
}
function seekProperty (prop, max, e) {
if (isNaN(max) || max === 0) return
let pos = relMouseCoords.call(this, e)
let clickPercent = pos.x / this.offsetWidth
if (max == 1) {
audio[prop] = clickPercent
return
}
audio[prop] = Math.floor(max * clickPercent)
}
function setClass (elem, int, rep) {
let classString = elem.className
if (classString.indexOf(int) !== -1) {
classString = classString.replace(int, rep)
} else if (classString.indexOf(rep) !== -1) {
return
}
elem.className = classString
}
function togglePlayButton () {
if (audio.paused) {
setClass(play, 'pause-btn', 'play-btn')
} else {
setClass(play, 'play-btn', 'pause-btn')
}
}
function registerSeekBar (element, v, mv) {
let seeking = false
let moved = false
element.addEventListener('mousedown', function (e) {
seeking = true
moved = false
}, false)
element.addEventListener('mousemove', function (e) {
if (!seeking) return
moved = true
if (v === 'currentTime') {
audio.seeking = true
}
seekProperty.call(element, v, (mv ? audio[mv] : 1), e)
})
element.addEventListener('mouseup', function (e) {
if (!seeking) return
seeking = false
if (moved) {
if (v === 'currentTime') {
audio.seeking = false
}
return
}
seekProperty.call(element, v, (mv ? audio[mv] : 1), e)
})
element.addEventListener('mouseleave', function (e) {
if (!seeking) return
seeking = false
updateSeeker()
})
}
audio.addEventListener('durationchange', updateSeeker, false)
audio.addEventListener('volumechange', updateVolume, false)
audio.addEventListener('timeupdate', updateSeeker, false)
audio.addEventListener('play', togglePlayButton, false)
audio.addEventListener('pause', togglePlayButton, false)
audio.addEventListener('stop', togglePlayButton, false)
play.addEventListener('click', function (e) {
if (audio.paused) {
audio.play()
} else {
audio.pause()
}
}, false)
mute.addEventListener('click', function (e) {
audio.muted = !audio.muted
updateVolume()
}, false)
registerSeekBar(seek, 'currentTime', 'duration')
registerSeekBar(vol, 'volume')
updateVolume()
})()