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:
torcado194 2022-04-14 21:27:05 -04:00
parent f1f685e7ac
commit b4eb5a1451
7 changed files with 325 additions and 32 deletions

View file

@ -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);
}