Locked tracks, preview tracks
Preview stripe colors Better horizontal spacing Sticky media on horizontal layout Better responsive width for controls/tracks Better volume positioning Percentage-based scrubber values for better responsive positioning
This commit is contained in:
parent
f1f685e7ac
commit
b4eb5a1451
7 changed files with 325 additions and 32 deletions
256
index.html
256
index.html
|
@ -17,7 +17,9 @@
|
|||
--secondaryColor: #000000;
|
||||
--highlightColor: rgba(131, 131, 131, 0.3);
|
||||
--primaryTextColor: #ffffff;
|
||||
--primaryAltTextColor: #838383;
|
||||
--primaryAltTextColor: hsl(0, 0%, 51%);
|
||||
--previewStripeColor1: #3f3f3f;
|
||||
--previewStripeColor2: #2c2c2c;
|
||||
--linkColor: #838383;
|
||||
--font: sans-serif;
|
||||
}
|
||||
|
@ -54,7 +56,7 @@
|
|||
#mainContainer {
|
||||
position: relative;
|
||||
display: grid;
|
||||
justify-content: space-around;
|
||||
justify-content: space-evenly;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
left: 0;
|
||||
|
@ -112,6 +114,8 @@
|
|||
#mainContainer #mediaColumn {
|
||||
grid-row: 2;
|
||||
grid-column: 1;
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,11 +126,16 @@
|
|||
grid-row: 1/3;
|
||||
grid-column: 2;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
position: sticky;
|
||||
top: 10px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
#mainContainer.vertical #mediaColumn {
|
||||
grid-row: 2;
|
||||
grid-column: 1;
|
||||
position: relative;
|
||||
top: 0;
|
||||
}
|
||||
#mainContainer.title-span #mediaColumn {
|
||||
grid-row: 2/3;
|
||||
|
@ -303,6 +312,7 @@
|
|||
flex-wrap: wrap;
|
||||
flex: 1 0;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#controls .title {
|
||||
|
@ -319,6 +329,7 @@
|
|||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
user-select: none;
|
||||
flex-basis: 8px;
|
||||
}
|
||||
|
||||
#controls .riser {
|
||||
|
@ -330,6 +341,7 @@
|
|||
align-self: end;
|
||||
color: var(--primaryAltTextColor);
|
||||
user-select: none;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#controls #scrubberTrackContainer {
|
||||
|
@ -341,6 +353,26 @@
|
|||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
#controls #scrubberTrackFull {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: repeating-linear-gradient( 135deg, var(--previewStripeColor1), var(--previewStripeColor1) 8px, var(--previewStripeColor2) 8px, var(--previewStripeColor2) 16px);
|
||||
border-radius: 12px;
|
||||
}
|
||||
#controls #scrubberTrackContainer #scrubberTrackPreview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
#controls #scrubberTrackContainer.preview #scrubberTrackPreview {
|
||||
border: 2px solid var(--backgroundColor);
|
||||
box-sizing: content-box;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
margin-right: -4px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
#controls #scrubberTrack {
|
||||
background-color: var(--primaryAltColor);
|
||||
overflow: hidden;
|
||||
|
@ -388,6 +420,9 @@
|
|||
align-items: center;
|
||||
user-select: none;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#controls #volumeContainer #volumeTrackContainer {
|
||||
|
@ -458,7 +493,7 @@
|
|||
border-radius: 6px;
|
||||
}
|
||||
.track.locked {
|
||||
opacity: 0.35;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.track.active:before {
|
||||
content: '';
|
||||
|
@ -573,6 +608,7 @@
|
|||
color: var(--primaryAltTextColor);
|
||||
font-size: 14px;
|
||||
flex: 0 0 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.track .spacer {
|
||||
flex: 1 1;
|
||||
|
@ -667,10 +703,14 @@
|
|||
<div class="title">____</div>
|
||||
<div class="time"></div>
|
||||
<button id="scrubberTrackContainer">
|
||||
<div id="scrubberTrack">
|
||||
<div id="scrubberFill"></div>
|
||||
<div id="scrubberTrackFull">
|
||||
<div id="scrubberTrackPreview">
|
||||
<div id="scrubberTrack">
|
||||
<div id="scrubberFill"></div>
|
||||
</div>
|
||||
<div id="scrubber"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scrubber"></div>
|
||||
</button>
|
||||
<button id="prevTrack" class="icon-previous"></button>
|
||||
<button id="nextTrack" class="icon-next"></button>
|
||||
|
@ -700,6 +740,7 @@
|
|||
let bigButtonEl = document.getElementById('bigButton');
|
||||
let controlsEl = document.getElementById('controls');
|
||||
let scrubberTrackContainerEl = document.getElementById('scrubberTrackContainer');
|
||||
let scrubberTrackPreviewEl = document.getElementById('scrubberTrackPreview');
|
||||
let scrubberTrackEl = document.getElementById('scrubberTrack');
|
||||
let scrubberFillEl = document.getElementById('scrubberFill');
|
||||
let scrubberEl = document.getElementById('scrubber');
|
||||
|
@ -751,6 +792,8 @@
|
|||
if(theme.secondaryColor) rootEl.style.setProperty('--secondaryColor', theme.secondaryColor);
|
||||
if(theme.highlightColor) rootEl.style.setProperty('--highlightColor', theme.highlightColor);
|
||||
if(theme.backgroundColor) rootEl.style.setProperty('--backgroundColor', theme.backgroundColor);
|
||||
if(theme.previewStripeColor1) rootEl.style.setProperty('--previewStripeColor1', theme.previewStripeColor1);
|
||||
if(theme.previewStripeColor2) rootEl.style.setProperty('--previewStripeColor2', theme.previewStripeColor2);
|
||||
if(theme.linkColor) rootEl.style.setProperty('--linkColor', theme.linkColor);
|
||||
if(theme.layoutStyle){
|
||||
document.getElementById('mainContainer').classList.remove('vertical', 'horizontal');
|
||||
|
@ -764,7 +807,7 @@
|
|||
document.getElementById('mainContainer').classList.remove('title-none', 'title-span');
|
||||
document.getElementById('mainContainer').classList.add('title-' + theme.titleStyle);
|
||||
}
|
||||
if(theme.contentWidth) document.getElementById('contentContainer').style.width = theme.contentWidth + 'px';
|
||||
if(theme.contentWidth) document.getElementById('contentContainer').style.maxWidth = theme.contentWidth + 'px';
|
||||
if(theme.nativePlayer){
|
||||
document.getElementById('audio').classList.add('native');
|
||||
mediaVideoEl.setAttribute('controls', '');
|
||||
|
@ -849,6 +892,8 @@
|
|||
updateDescription(description);
|
||||
updateTheme(theme);
|
||||
|
||||
updateTrackPreview();
|
||||
|
||||
document.title = titleEl.textContent;
|
||||
|
||||
volume = parseFloat(localStorageGet('volume'));
|
||||
|
@ -916,6 +961,9 @@
|
|||
infoContainerEl.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
if(!entry.previewFade && entry.previewFade !== false){
|
||||
entry.previewFade = true; //default true
|
||||
}
|
||||
if(!entry.locked){
|
||||
trackEl.querySelector('button').onclick = () => {
|
||||
loadedFirst = true;
|
||||
|
@ -928,11 +976,51 @@
|
|||
} else {
|
||||
loaderEl = document.createElement('audio');
|
||||
}
|
||||
if(entry.previewStart || entry.previewEnd){
|
||||
entry.preview = true;
|
||||
}
|
||||
let needsPreviewEnd = false;
|
||||
if(!entry.previewEnd){
|
||||
needsPreviewEnd = true;
|
||||
}
|
||||
entry.originalDuration = (entry.originalDuration && fromHMS(entry.originalDuration)) || 1;
|
||||
entry.previewStart = (entry.previewStart && fromHMS(entry.previewStart)) || 0;
|
||||
entry.previewEnd = (entry.previewEnd && fromHMS(entry.previewEnd)) || entry.originalDuration;
|
||||
entry.previewDuration = (entry.previewEnd - entry.previewStart);
|
||||
loaderEl.addEventListener('loadedmetadata', e => {
|
||||
entry.duration = e.target.duration;
|
||||
trackEl.querySelector('.duration').textContent = toHMS(entry.duration);
|
||||
entry.needsDuration = false;
|
||||
entry.previewStart = entry.previewStart || 0;
|
||||
if(needsPreviewEnd){
|
||||
entry.previewEnd = entry.duration;
|
||||
}
|
||||
entry.originalDuration = Math.max(entry.originalDuration, entry.duration, entry.previewEnd);
|
||||
entry.previewDuration = (entry.previewEnd - entry.previewStart);
|
||||
|
||||
//make assumptions about the audio start points
|
||||
if(entry.duration < entry.previewDuration){ //smaller than given preview duration
|
||||
//left align to preview
|
||||
entry.audioStart = entry.previewStart;
|
||||
} else if(entry.duration < entry.originalDuration - entry.previewStart){ //else, smaller than region between preview start and original end
|
||||
//left align to preview
|
||||
entry.audioStart = entry.previewStart;
|
||||
} else if(entry.duration < entry.originalDuration){ //else, smaller than original duration
|
||||
//right align to original
|
||||
entry.audioStart = entry.originalDuration - entry.duration;
|
||||
} else { //larger than original duration
|
||||
//left align to original
|
||||
entry.audioStart = 0;
|
||||
}
|
||||
entry.audioEnd = entry.audioStart + entry.duration;
|
||||
|
||||
let durationText = toHMS(entry.duration);
|
||||
if(entry.preview){
|
||||
durationText += ' (preview)';
|
||||
}
|
||||
trackEl.querySelector('.duration').textContent = durationText;
|
||||
if(entry === currentEntry){
|
||||
updateScrubPosition(0);
|
||||
updateTrackPreview();
|
||||
updateScrubPosition(entry.previewStart / entry.originalDuration);
|
||||
}
|
||||
});
|
||||
loaderEl.addEventListener('error', e => {
|
||||
|
@ -947,15 +1035,14 @@
|
|||
});
|
||||
loaderEl.volume = 0;
|
||||
loaderEl.src = entry.file;
|
||||
entry.started = false;
|
||||
entry.loading = true;
|
||||
}
|
||||
});
|
||||
loadEntry(media[featureIndex]);
|
||||
loadEntry(media[mod(featureIndex, media.length)]);
|
||||
}
|
||||
|
||||
function loadCover(cover){
|
||||
// let el = document.createElement('img');
|
||||
// mediaImageEl.appendChild(el);
|
||||
let src = cover;
|
||||
if(!/:\/\//.test(src)){ //if not a url
|
||||
src = mediaDir + src;
|
||||
|
@ -966,7 +1053,15 @@
|
|||
|
||||
playerEl.addEventListener('canplay', e => {
|
||||
removeLoading(currentEntry);
|
||||
if(autoPlay){
|
||||
if(currentEntry.needsDuration){
|
||||
currentEntry.duration = playerEl.duration;
|
||||
currentEntry.needsDuration = false;
|
||||
}
|
||||
if(autoPlay && !currentEntry.started){
|
||||
currentEntry.started = true;
|
||||
if(!currentEntry.needsDuration){
|
||||
playerEl.currentTime = currentEntry.previewStart - currentEntry.audioStart;
|
||||
}
|
||||
playerEl.play();
|
||||
}
|
||||
if(!loadedFirst){
|
||||
|
@ -977,7 +1072,11 @@
|
|||
setLoading(currentEntry, 0);
|
||||
});
|
||||
playerEl.addEventListener('play', e => {
|
||||
if(currentEntry.preview && playerEl.currentTime < currentEntry.previewStart - currentEntry.audioStart){
|
||||
playerEl.currentTime = currentEntry.previewStart - currentEntry.audioStart;
|
||||
}
|
||||
bigButtonEl.classList.add('pause');
|
||||
currentEntry.started = true;
|
||||
currentEntry.trackEl.querySelector('button').classList.add('pause');
|
||||
if(currentEntry.type === 'video'){
|
||||
mediaVideoEl.play();
|
||||
|
@ -1011,8 +1110,20 @@
|
|||
});
|
||||
playerEl.addEventListener('timeupdate', e => {
|
||||
if(!scrubberDragged){
|
||||
let t = e.target.currentTime / currentEntry.duration;
|
||||
updateScrubPosition(t);
|
||||
if(currentEntry.duration && !currentEntry.needsDuration){
|
||||
if(currentEntry.started){
|
||||
let time = e.target.currentTime;
|
||||
if(time > currentEntry.previewEnd - currentEntry.audioStart){
|
||||
nextTrack(false);
|
||||
} else {
|
||||
let t = time / currentEntry.duration;
|
||||
updatePreviewVolume(t);
|
||||
updateScrubPosition(t);
|
||||
}
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
playerEl.addEventListener('ended', e => {
|
||||
|
@ -1108,6 +1219,7 @@
|
|||
let scrubberDragged = false;
|
||||
scrubberTrackContainerEl.addEventListener('pointerdown', e => {
|
||||
scrubberDragged = true;
|
||||
currentEntry.started = true;
|
||||
scrub(e.clientX);
|
||||
});
|
||||
document.addEventListener('pointerup', e => {
|
||||
|
@ -1135,15 +1247,63 @@
|
|||
prevTrack();
|
||||
});
|
||||
|
||||
function updateTrackPreview(){
|
||||
if(currentEntry.needsDuration){
|
||||
return;
|
||||
}
|
||||
//reset to prevent compounding width calculation issues
|
||||
scrubberTrackPreviewEl.style.removeProperty('margin-left');
|
||||
scrubberTrackPreviewEl.style.removeProperty('width');
|
||||
scrubberEl.style.removeProperty('left');
|
||||
scrubberFillEl.style.removeProperty('width');
|
||||
if(currentEntry.preview){
|
||||
let fullWidth = scrubberEl.parentElement.parentElement.clientWidth;
|
||||
scrubberTrackPreviewEl.style.marginLeft = ((currentEntry.previewStart) / currentEntry.originalDuration) * 100 + '%';
|
||||
scrubberTrackPreviewEl.style.width = ((currentEntry.previewEnd - currentEntry.previewStart) / currentEntry.originalDuration) * 100 + '%';
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreviewVolume(t){
|
||||
if(currentEntry.preview && currentEntry.previewFade && !muted){
|
||||
if(currentEntry.needsDuration){
|
||||
playEntry.volume = 0;
|
||||
} else {
|
||||
//rebase t to full (assumed) duration
|
||||
t = currentEntry.audioStart + (t * currentEntry.duration);
|
||||
//clip to preview
|
||||
t = Math.max(currentEntry.previewStart, Math.min(currentEntry.previewEnd, t));
|
||||
let inTime = t - currentEntry.previewStart;
|
||||
let outTime = currentEntry.previewEnd - t;
|
||||
playerEl.volume = volume * Math.max(0, Math.min(1, inTime/1, outTime/1)); //1 second fade in/out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//t: 0-1 value in relation to the actual audio start/end
|
||||
function updateScrubPosition(t){
|
||||
scrubPosition = t;
|
||||
//rebase t to full (assumed) duration
|
||||
t = currentEntry.audioStart + (t * currentEntry.duration);
|
||||
//clip to preview
|
||||
t = Math.max(currentEntry.previewStart, Math.min(currentEntry.previewEnd, t));
|
||||
scrubPosition = (t - currentEntry.audioStart) / currentEntry.duration;
|
||||
let time = t;
|
||||
//rebase
|
||||
t = t / currentEntry.originalDuration;
|
||||
//relative to preview region
|
||||
let scrubberT = ((t * currentEntry.originalDuration) - currentEntry.previewStart) / currentEntry.previewDuration;
|
||||
let scrubberWidth = scrubberEl.clientWidth;
|
||||
let trackWidth = scrubberEl.parentElement.clientWidth;
|
||||
//scrubberEl.style.left = `calc(${(t * 100).toFixed(0)}% - 8px)`;
|
||||
scrubberEl.style.left = (scrubPosition * (trackWidth - scrubberWidth)) + 'px';
|
||||
scrubberFillEl.style.width = (scrubPosition * trackWidth) + 'px';
|
||||
let time = scrubPosition * currentEntry.duration;
|
||||
controlsEl.querySelector('.time').textContent = `${toHMS(time)} / ${currentEntry.duration ? toHMS(currentEntry.duration) : "0:00"}`;
|
||||
|
||||
scrubberEl.style.left = ((scrubberT * (trackWidth - scrubberWidth)) / trackWidth) * 100 + '%';
|
||||
scrubberFillEl.style.width = (scrubberT) * 100 + '%';
|
||||
|
||||
//let time = timePosition * currentEntry.duration;
|
||||
let timeText = '';
|
||||
if(currentEntry.preview){
|
||||
timeText += '(preview)<br/>';
|
||||
}
|
||||
timeText += `${toHMS(time)} / ${currentEntry.originalDuration ? toHMS(currentEntry.originalDuration) : "0:00"}`
|
||||
controlsEl.querySelector('.time').innerHTML = timeText;
|
||||
}
|
||||
|
||||
function scrub(x){
|
||||
|
@ -1153,6 +1313,10 @@
|
|||
let scrubberWidth = scrubberEl.clientWidth;
|
||||
let trackWidth = scrubberEl.parentElement.clientWidth;
|
||||
let t = Math.max(0, Math.min(1, (x - scrubberWidth/2) / (trackWidth - scrubberWidth)));
|
||||
if(currentEntry.preview){
|
||||
//rebase t from preview to audio start/end
|
||||
t = (t * currentEntry.previewDuration + (currentEntry.previewStart - currentEntry.audioStart)) / currentEntry.duration;
|
||||
}
|
||||
updateScrubPosition(t);
|
||||
}
|
||||
}
|
||||
|
@ -1189,8 +1353,8 @@
|
|||
loadEntry(entry);
|
||||
}
|
||||
function prevTrack(){
|
||||
if(playerEl.currentTime > 5){
|
||||
playerEl.currentTime = 0;
|
||||
if(playerEl.currentTime > (currentEntry.previewStart - currentEntry.audioStart) + 5){
|
||||
playerEl.currentTime = (currentEntry.previewStart - currentEntry.audioStart);
|
||||
} else {
|
||||
let idx = media.indexOf(currentEntry)-1;
|
||||
let entry = media[mod(idx, media.length)];
|
||||
|
@ -1244,7 +1408,15 @@
|
|||
});
|
||||
currentEntry = entry;
|
||||
if(!currentEntry.duration){
|
||||
currentEntry.duration = 0.01;
|
||||
currentEntry.duration = 999;
|
||||
currentEntry.needsDuration = true;
|
||||
}
|
||||
updateTrackPreview();
|
||||
if(entry.preview){
|
||||
scrubberTrackContainerEl.classList.add('preview');
|
||||
updatePreviewVolume(0);
|
||||
} else {
|
||||
scrubberTrackContainerEl.classList.remove('preview');
|
||||
}
|
||||
Array.from(mediaContainerEl.children).forEach(el => {
|
||||
el.classList.remove('active');
|
||||
|
@ -1271,6 +1443,7 @@
|
|||
mediaVideoEl.load();
|
||||
}
|
||||
playerEl.src = entry.file;
|
||||
entry.started = false;
|
||||
setLoading(currentEntry);
|
||||
entry.trackEl.classList.add('active');
|
||||
controlsEl.querySelector('.title').innerHTML = entry.title;
|
||||
|
@ -1301,7 +1474,7 @@
|
|||
|
||||
|
||||
function toHMS(secs){
|
||||
let sec_num = parseInt(secs, 10);
|
||||
let sec_num = Math.max(0, Math.round(secs)); //flooring is more correct, but rounding gives nicer results due to playback event buffering
|
||||
let hours = Math.floor(sec_num / 3600);
|
||||
let minutes = Math.floor(sec_num / 60) % 60;
|
||||
let seconds = sec_num % 60;
|
||||
|
@ -1312,6 +1485,37 @@
|
|||
.join(":")
|
||||
}
|
||||
|
||||
function fromHMS(hms){
|
||||
if(!hms){
|
||||
return 0;
|
||||
}
|
||||
let groups = (''+hms).split(':');
|
||||
if(groups.length < 1){
|
||||
return 0;
|
||||
}
|
||||
for(let i = 0; i < groups.length; i++){
|
||||
if(groups[i] === ''){
|
||||
groups[i] = '0';
|
||||
continue;
|
||||
}
|
||||
if(isNaN(groups[i])){
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
let seconds = 0;
|
||||
let minutes = 0;
|
||||
let hours = 0;
|
||||
seconds = parseFloat(groups[groups.length-1]);
|
||||
if(groups.length >= 2){
|
||||
minutes = parseFloat(groups[groups.length-2]);
|
||||
}
|
||||
if(groups.length >= 3){
|
||||
hours = parseFloat(groups[groups.length-3]);
|
||||
}
|
||||
|
||||
return (((hours * 60) + minutes) * 60) + seconds;
|
||||
}
|
||||
|
||||
function mod(x, n){
|
||||
return (((x % n) + n) % n);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue