scritch-player/index.html
2023-09-28 18:59:28 -04:00

1709 lines
No EOL
53 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>itch player</title>
<link rel="stylesheet" href="src/reset.css">
<link rel="stylesheet" href="src/common.css">
<link rel="stylesheet" href="src/fonts.css">
<!-- <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@600&text=0123456789&display=swap" rel="stylesheet"> -->
</head>
<style>
:root {
--backgroundColor: #1E1E1E;
--primaryColor: #ffffff;
--primaryAltColor: #838383;
--secondaryColor: #000000;
--highlightColor: rgba(131, 131, 131, 0.3);
--primaryTextColor: #ffffff;
--primaryAltTextColor: hsl(0, 0%, 51%);
--previewStripeColor1: #3f3f3f;
--previewStripeColor2: #2c2c2c;
--linkColor: #838383;
--font: sans-serif;
}
@font-face {
font-family: 'Rubik-numbers';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('src/fonts/rubikNumbers-edit.woff2') format('woff2');
}
@keyframes pulse {
0% {
opacity: 0.5;
}
50% {
opacity: 1.0;
}
100% {
opacity: 0.5;
}
}
/* basic layout */
html, body {
width: 100%;
height: 100%;
overflow: auto;
}
body {
color: var(--primaryTextColor);
background-color: var(--backgroundColor);
font-family: var(--font);
}
a {
color: var(--linkColor)
}
#mainContainer {
position: relative;
display: grid;
justify-content: space-evenly;
width: 100%;
max-width: 960px;
left: 0;
right: 0;
margin: auto;
}
#mainContainer.vertical {
grid-template-columns: auto;
}
button {
font-family: 'Segoe UI Symbol', sans-serif;
}
/*** title ***/
#titleContainer {
height: 0;
}
#titleContainer.active {
height: auto;
}
#title {
margin: 20px 20px 4px 20px;
font-size: 26px;
display: none;
}
#titleContainer.active #title {
display: block;
}
#mainContainer.vertical #title {
margin: 20px;
text-align: center;
}
#mainContainer.title-span #title {
margin: 20px;
text-align: center;
}
#mainContainer:not(.vertical).title-span #titleContainer {
grid-column: 1/3;
}
@media (max-width: 590px) {
#mainContainer #title {
margin: 20px;
text-align: center;
}
#mainContainer.title-span.title-span #titleContainer {
grid-column: 1;
}
#mainContainer {
grid-template-columns: auto;
}
#mainContainer #mediaColumn {
grid-row: 2;
grid-column: 1;
position: relative;
top: 0;
}
}
/*** media column ***/
#mediaColumn {
display: flex;
grid-row: 1/3;
grid-column: 2;
flex-direction: column;
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;
}
.info-overlaid #mediaColumn, .info-overlaid-toggle #mediaColumn {
max-width: 480px;
height: fit-content;
}
#mediaContainer {
width: 100%;
max-width: 480px;
display: flex;
position: relative;
overflow: hidden;
}
#mediaContainer > * {
width: 100%;
/* display: none; */
visibility: hidden;
margin-right: -100%;
}
#mediaContainer .active {
/* display: block; */
visibility: visible;
}
#mediaContainer .active {
/* display: block; */
visibility: visible;
}
#mediaInfoContainer {
display: none;
overflow-y: auto;
background-color: var(--highlightColor);
border-radius: 8px;
}
#mediaInfoContainer.active {
display: block;
padding: 10px;
margin-top: 12px;
}
.info-overlaid #mediaInfoContainer, .info-overlaid-toggle #mediaInfoContainer {
padding: 30px;
margin: 0;
position: absolute;
backdrop-filter: blur(3px);
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 0;
display: flex;
text-align: center;
font-size: 20px;
opacity: 0;
transition: opacity 0.25s;
}
.info-overlaid #mediaInfoContainer.active, .info-overlaid-toggle #mediaInfoContainer.active {
opacity: 1;
}
.info-overlaid #mediaInfo, .info-overlaid-toggle #mediaInfo {
margin: auto;
position: relative;
top: 0;
bottom: 0;
}
.info-below #mediaInfoContainer {
display: block;
opacity: 0;
transition: opacity 0.25s;
}
.info-below #mediaInfoContainer.active {
opacity: 1;
}
.info-none #mediaInfoContainer {
display: none !important;
}
/*** content column ***/
#contentColumn {
min-width: 270px;
grid-column: 1;
}
#contentContainer {
padding: 0 10px;
margin: auto;
}
/* player controls */
#audio {
width: 100%;
display: flex;
margin-bottom: 20px;
user-select: none;
}
#player {
width: 100%;
display: none;
}
#audio.native {
height: 70px;
}
#audio.native #player {
display: block;
}
#audio.native #player.video {
visibility: hidden;
pointer-events: none;
}
#bigButton {
width: 54px;
height: 54px;
background-color: var(--primaryColor);
border-radius: 50%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0;
flex-basis: 54px;
margin-left: 0;
cursor: pointer;
margin-top: 26px;
margin-right: 22px;
}
#bigButton:not(.loading):after {
content: '';
display: block;
width: 0;
height: 0;
border-top: 14px inset transparent;
border-bottom: 14px inset transparent;
border-left: 23px solid var(--secondaryColor);
border-right: 0;
margin-left: 5px;
}
#bigButton:not(.loading).pause:after {
content: '';
border: 0;
border-left: 8px solid var(--secondaryColor);
border-right: 8px solid var(--secondaryColor);
height: 24px;
margin: 4px auto;
width: 2px;
padding: 3px;
left: 0;
}
#bigButton.loading:after {
content: "\25cc";
color: var(--secondaryColor);
font-family: 'icons' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
font-size: 40px;
animation: pulse 1.0s infinite;
}
#bigButton.looped:before {
content: attr(loopcount);
display: block;
width: 20px;
height: 20px;
position: absolute;
top: -6px;
right: -6px;
color: var(--secondaryColor);
background-color: var(--primaryColor);
border: 2px solid var(--backgroundColor);
border-radius: 20px;
font-family: 'Rubik-numbers';
font-weight: 600;
font-size: 14px;
text-align: center;
}
#controls {
display: flex;
flex-wrap: wrap;
flex: 1 0;
align-items: center;
position: relative;
}
#controls .title {
flex: 1 0;
font-weight: 600;
align-self: end;
font-size: 28px;
line-height: 28px;
width: 100px;
word-break: break-word;
overflow: hidden;
/* height: 60px; */
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
user-select: none;
flex-basis: 8px;
}
#controls .riser {
height: 60px;
}
#controls .time {
font-size: 12px;
align-self: end;
color: var(--primaryAltTextColor);
user-select: none;
text-align: right;
}
#controls #scrubberTrackContainer {
flex-basis: 100%;
height: 16px;
position: relative;
padding: 4px 0;
user-select: none;
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;
width: 100%;
height: 100%;
border-radius: 12px;
position: relative;
}
#scrubberFill {
position: absolute;
height: 100%;
background-color: var(--primaryColor);
content: '';
}
#controls #scrubber {
box-sizing: content-box;
width: 8px;
height: 8px;
background-color: var(--primaryColor);
border: 2px solid var(--backgroundColor);
border-radius: 50%;
position: absolute;
top: 0;
bottom: 0;
margin: auto -2px;
}
#controls #nextTrack, #controls #prevTrack, #controls #loopSwitch {
border: none;
margin-right: 20px;
flex: 0 0;
background: none;
color: var(--primaryColor);
padding: 0;
align-self: start;
cursor: pointer;
}
#controls #loopSwitch {
opacity: 0.5;
}
#controls .spacer {
flex: 1 1;
}
#controls #volumeContainer {
display: flex;
align-items: center;
user-select: none;
height: 20px;
position: absolute;
right: 0;
bottom: 0;
}
#controls #volumeContainer #volumeTrackContainer {
flex: 1 0;
cursor: pointer;
width: 0px;
height: 100%;
overflow: hidden;
position: relative;
align-self: start;
transition: width 0.2s;
touch-action: none;
}
#controls #volumeContainer.hover #volumeTrackContainer {
width: 80px;
}
#controls #volumeContainer #volumeTrackContainer #volumeTrack {
background-color: var(--primaryAltColor);
width: 80px;
height: 6px;
border-radius: 10px;
position: absolute;
right: 0;
overflow: hidden;
margin: auto;
top: 0;
bottom: 0;
}
#controls #volumeContainer #volumeTrackContainer #volumeFill {
background-color: var(--primaryColor);
position: absolute;
width: 100%;
height: 100%;
border-radius: 10px;
border: 2px solid var(--backgroundColor);
box-sizing: content-box;
margin: -2px;
}
#controls #volumeContainer #volumeIcon {
flex: 0 0;
align-self: start;
cursor: pointer;
margin-left: 6px;
cursor: pointer;
color: var(--primaryColor);
}
#audio.native #controls, #audio.native #bigButton {
display: none;
}
/* track list */
#tracks {
display: flex;
flex-direction: column;
width: 100%;
}
.track {
display: flex;
position: relative;
overflow: hidden;
flex-wrap: wrap;
border-radius: 6px;
}
.track.locked {
opacity: 0.5;
}
.track.active:before {
content: '';
width: 100%;
height: 100%;
opacity: 0.3;
background-color: var(--highlightColor);
position: absolute;
pointer-events: none;
}
.track.active .main {
background-color: var(--highlightColor);
align-items: center;
}
.track .main {
display: flex;
flex: 1 0 100%;
padding: 6px;
border-radius: 6px;
align-items: center;
}
.track button {
margin-right: 12px;
width: 26px;
height: 26px;
background-color: var(--primaryColor);
color: var(--secondaryColor);
border-radius: 3px;
flex: 0 0 26px;
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.track button:not(.loading, .error, .locked):after {
content: '';
display: block;
width: 0px;
height: 0;
border-top: 6px inset transparent;
border-bottom: 6px inset transparent;
border-left: 10px solid var(--secondaryColor);
border-right: 0;
margin: auto;
position: relative;
left: 1px;
}
.track button:not(.loading, .error, .locked).pause:after {
content: '';
border: 0;
border-left: 4px solid var(--secondaryColor);
border-right: 4px solid var(--secondaryColor);
height: 12px;
margin: 4px auto;
width: 2px;
padding: 1px;
left: 0;
}
.track button.loading.loading:after {
content: "\1f506";
color: var(--secondaryColor);
font-family: 'icons' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
font-size: 20px;
animation: pulse 1.0s infinite;
}
.track button.error {
opacity: 0.5;
cursor: default;
}
.track button.locked {
cursor: default;
}
.track button.locked:after {
content: "\1f512";
color: var(--secondaryColor);
font-family: 'icons' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
font-size: 20px;
}
.track.looped button:before {
content: attr(loopcount);
display: block;
width: 15px;
height: 15px;
position: absolute;
top: -6px;
right: -6px;
color: var(--secondaryColor);
background-color: var(--primaryColor);
border: 2px solid var(--backgroundColor);
border-radius: 20px;
font-family: 'Rubik-numbers';
font-weight: 600;
font-size: 10px;
text-align: center;
}
.track.active.looped button:before {
border-color: var(--secondaryColor);;
}
.track .title {
word-break: break-word;
}
.track .details {
display: flex;
margin-top: 2px;
align-items: center;
flex-basis: 100%;
}
.track .number {
margin-right: 4px;
color: var(--primaryAltTextColor);
}
.track .duration {
margin-left: 14px;
color: var(--primaryAltTextColor);
font-size: 14px;
flex: 0 0 30px;
white-space: nowrap;
}
.track .spacer {
flex: 1 1;
}
.track .toggleInfo {
margin-left: 8px;
margin-right: 4px;
cursor: pointer;
color: var(--primaryAltTextColor);
}
.track.active .toggleInfo {
color: var(--primaryColor);
}
.track .infoContainer {
width: 100%;
transition: height 0.5s;
}
.track .infoContainer:not(.active) {
height: 0px !important;
}
.track .info {
padding: 10px;
padding-left: 45px;
}
/* description */
#description {
display: none;
padding: 8px;
margin-top: 24px;
word-break: break-word;
margin-bottom: 40px;
}
#description.active {
display: block;
}
/*** layered style ***/
#mainContainer.overlaid {
width: fit-content;
margin: auto;
}
#mainContainer.overlaid #contentColumn {
position: absolute;
display: flex;
flex-direction: column;
max-height: 100%;
overflow: hidden;
backdrop-filter: blur(5px);
width: 100%;
max-width: unset !important;
padding: 0 14px;
}
#mainContainer.overlaid #tracks {
overflow-y: auto;
}
</style>
<body>
<div id="mainContainer">
<div id="titleContainer">
<div id="title">
</div>
</div>
<div id="mediaColumn">
<div id="mediaContainer">
<img id="mediaImage"></img>
<video id="mediaVideo"></video>
</div>
<div id="mediaInfoContainer">
<div id="mediaInfo">
</div>
</div>
</div>
<div id="contentColumn">
<div id="contentContainer">
<div id="audio">
<audio id="player" controls src="">
</audio>
<button id="bigButton"></button>
<div id="controls">
<div class="riser"></div>
<div class="title">____</div>
<div class="time"></div>
<button id="scrubberTrackContainer">
<div id="scrubberTrackFull">
<div id="scrubberTrackPreview">
<div id="scrubberTrack">
<div id="scrubberFill"></div>
</div>
<div id="scrubber"></div>
</div>
</div>
</button>
<button id="prevTrack" class="icon-previous"></button>
<button id="nextTrack" class="icon-next"></button>
<button id="loopSwitch" class="icon-loop" title="loop off"></button>
<div class="spacer"></div>
<div id="volumeContainer">
<div id="volumeTrackContainer">
<div id="volumeTrack">
<div id="volumeFill"></div>
</div>
</div>
<div id="volumeIcon" class="icon-volume-high"></div>
</div>
</div>
</div>
<div id="tracks"></div>
<div id="description">
</div>
</div>
</div>
</div>
</body>
<script>
let mediaDir = 'media/';
let playerEl = document.getElementById('player');
let tracksEl = document.getElementById('tracks');
let descriptionEl = document.getElementById('description');
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');
let nextTrackEl = document.getElementById('nextTrack');
let prevTrackEl = document.getElementById('prevTrack');
let loopSwitchEl = document.getElementById('loopSwitch');
let titleEl = document.getElementById('title');
let titleContainerEl = document.getElementById('titleContainer');
let mediaColumnEl = document.getElementById('mediaColumn');
let mediaContainerEl = document.getElementById('mediaContainer');
let mediaInfoEl = document.getElementById('mediaInfo');
let mediaImageEl = document.getElementById('mediaImage');
let mediaVideoEl = document.getElementById('mediaVideo');
let volumeContainerEl = document.getElementById('volumeContainer');
let volumeTrackContainerEl = document.getElementById('volumeTrackContainer');
let volumeTrackEl = document.getElementById('volumeTrack');
let volumeFillEl = document.getElementById('volumeFill');
let volumeIconEl = document.getElementById('volumeIcon');
let currentEntry = {};
let media = [];
let scrubPosition = 0;
let volumeIcons = ['icon-volume-none', 'icon-volume-low', 'icon-volume-high'];
let volume = 0.8;
let muted = false;
let loadedFirst = false;
let autoPlay = false;
let loopMode = "none";
let config = {};
function init(){
fetch('config.json').then(res => res.json()).then(data => {
update(data);
}).catch(console.error);
}
init();
function updateTheme(theme){
config.theme = theme;
let rootEl = document.documentElement;
if(theme.primaryColor) rootEl.style.setProperty('--primaryColor', theme.primaryColor);
if(theme.primaryAltColor) rootEl.style.setProperty('--primaryAltColor', theme.primaryAltColor);
if(theme.primaryTextColor) rootEl.style.setProperty('--primaryTextColor', theme.primaryTextColor);
if(theme.primaryAltTextColor) rootEl.style.setProperty('--primaryAltTextColor', theme.primaryAltTextColor);
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');
document.getElementById('mainContainer').classList.add(theme.layoutStyle);
}
if(theme.infoStyle){
document.getElementById('mainContainer').classList.remove('info-none', 'info-overlaid', 'info-below', 'info-overlaid-toggle');
document.getElementById('mainContainer').classList.add('info-' + theme.infoStyle);
}
if(theme.titleStyle){
document.getElementById('mainContainer').classList.remove('title-none', 'title-span');
document.getElementById('mainContainer').classList.add('title-' + theme.titleStyle);
}
if(theme.contentWidth) document.getElementById('contentContainer').style.maxWidth = theme.contentWidth + 'px';
if(theme.nativePlayer){
document.getElementById('audio').classList.add('native');
mediaVideoEl.setAttribute('controls', '');
} else {
document.getElementById('audio').classList.remove('native');
mediaVideoEl.removeAttribute('controls');
}
mediaContainerEl.style.maxWidth = theme.coverSize + 'px';
mediaContainerEl.style.maxHeight = theme.coverSize + 'px';
mediaColumnEl.style.maxWidth = theme.coverSize + 'px';
if(theme.customCSS !== undefined){
document.querySelectorAll('#customCSS').forEach(el => el.remove());
let styleEl = document.createElement('style');
styleEl.setAttribute('id', 'customCSS');
styleEl.innerText = theme.customCSS;
document.head.appendChild(styleEl);
}
}
function loadContent(data){
loadCover(data.cover);
mediaImageEl.style.width = config.theme.coverSize + 'px';
mediaImageEl.style.height = config.theme.coverSize + 'px';
if(data.media) loadMedia(data.media);
}
function updateTitle(title){
if(title){
titleContainerEl.classList.add('active');
config.title = title;
titleEl.innerHTML = title;
} else {
titleContainerEl.classList.remove('active');
}
}
function updateDescription(description){
if(description === undefined || description === ''){
descriptionEl.classList.remove('active');
} else {
descriptionEl.classList.add('active');
config.description = description;
descriptionEl.innerHTML = description;
}
}
function updateLoopMode(mode){
if(mode === undefined || mode === ''){
config.loopModeDefault = 'none';
} else {
config.loopModeDefault = mode;
}
loopSwitch(config.loopModeDefault);
}
function localStorageSet(key, value){
try {
localStorage.setItem(key, value);
} catch {
}
}
function localStorageGet(key){
let value;
try {
value = localStorage.getItem(key);
} catch {
return undefined;
}
return value;
}
function update(data){
if(!data){
data = config;
}
if(!data.theme){
data.theme = {};
}
let title = data.title;
let theme = data.theme;
let description = data.description;
config = data;
loadContent(data);
updateTitle(title)
updateDescription(description);
updateLoopMode(data.loopModeDefault)
updateTheme(theme);
updateTrackPreview();
document.title = titleEl.textContent;
volume = parseFloat(localStorageGet('volume'));
muted = parseInt(localStorageGet('muted'));
if(volume && typeof volume === 'number'){
setVolume(volume, muted);
} else {
volume = 0.8;
muted = false;
localStorageSet('volume', volume);
}
}
function loadMedia(list){
if(list.length === 0){
return;
}
loadedFirst = false;
autoPlay = false;
bigButtonEl.classList.remove('pause');
media = list.map(entry => {
//return {file: entry.file, title: entry.title};
if(!/:\/\//.test(entry.file)){ //if not a url
entry.file = mediaDir + entry.file;
}
entry.title = entry.title || entry.file;
return entry;
});
let featureIndex = 0;
tracksEl.textContent = '';
media.forEach((entry, i) => {
tracksEl.insertAdjacentHTML('beforeend',
`<div class="track ${entry.locked ? 'locked' : ''}" ${entry.locked ? 'title="This file is available in the full download"' : ''}>
<div class="main">
<button class="${entry.locked ? 'locked' : 'loading'}"></button>
<div class="details">
<div class="number">${i+1}. </div>
<div class="title">${entry.title}</div>
<div class="duration"></div>
<div class="spacer"></div>
${entry.info ? `<div class="toggleInfo icon-info"></div>` : ''}
</div>
</div>
${entry.info ? `
<div class="infoContainer">
<div class="info">
${entry.info}
</div>
</div>
` : ''}
</div>`);
let trackEl = tracksEl.lastChild;
entry.trackEl = trackEl;
let buttonEl = trackEl.querySelector('button');
if(entry.feature){
featureIndex = i;
}
if(entry.locked && featureIndex === i){
featureIndex++;
}
if(entry.info){
let infoContainerEl = trackEl.querySelector('.infoContainer');
trackEl.querySelector('.toggleInfo').addEventListener('click', (e) => {
infoContainerEl.style.height = trackEl.querySelector('.info').clientHeight + 'px';
entry.infoToggled = infoContainerEl.classList.toggle('active');
if(config.theme.infoStyle === 'overlaid-toggle'){
if(entry.info && entry.infoToggled){
mediaInfoEl.innerHTML = entry.info;
mediaInfoContainer.classList.add('active');
} else {
mediaInfoEl.innerHTML = '';
mediaInfoContainer.classList.remove('active');
}
}
});
}
if(!entry.previewFade && entry.previewFade !== false){
entry.previewFade = true; //default true
}
if(typeof entry.loopCount === 'number' && entry.loopCount > 0){
entry.remainingLoops = entry.loopCount;
trackEl.classList.add('looped');
buttonEl.setAttribute('title', 'loop count');
updateLoops(entry);
}
if(!entry.locked){
buttonEl.onclick = () => {
loadedFirst = true;
autoPlay = true;
playEntry(entry);
}
let loaderEl;
if(entry.type === 'video'){
loaderEl = document.createElement('video');
} 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;
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){
updateTrackPreview();
// updateScrubPosition(entry.previewStart / entry.originalDuration);
updateScrubPosition(0);
updatePreviewVolume(0);
}
});
loaderEl.addEventListener('error', e => {
entry.error = true;
entry.errorMessage = e.target.error;
removeLoading(entry, true);
buttonEl.classList.add('error', 'icon-warning');
buttonEl.setAttribute('title', 'error loading file');
});
loaderEl.addEventListener('canplay', e => {
removeLoading(entry, true);
if(entry.feature && entry.autoplay){
playerEl.play();
}
});
loaderEl.volume = 0;
loaderEl.src = entry.file;
entry.started = false;
entry.loading = true;
entry.loaderEl = loaderEl;
}
});
loadEntry(media[mod(featureIndex, media.length)]);
}
function loadCover(cover){
if(cover){
let src = cover;
if(mediaImageEl.src == src){
return; //already loaded
}
if(!/:\/\//.test(src)){ //if not a url
src = mediaDir + src;
}
mediaImageEl.src = src;
mediaImageEl.classList.add('active');
mediaImageEl.style.visibility = 'visible';
} else {
mediaImageEl.style.visibility = 'hidden';
}
}
playerEl.addEventListener('canplay', e => {
removeLoading(currentEntry);
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){
autoPlay = true;
}
});
playerEl.addEventListener('waiting', e => {
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();
}
});
playerEl.addEventListener('pause', e => {
bigButtonEl.classList.remove('pause');
currentEntry.trackEl.querySelector('button').classList.remove('pause');
if(currentEntry.type === 'video'){
mediaVideoEl.pause();
}
});
playerEl.addEventListener('volumechange', e => {
if(config.theme.nativePlayer){
volume = e.target.volume;
localStorageSet('volume', volume);
}
if(currentEntry.type === 'video'){
if(config.theme.nativePlayer){
mediaVideoEl.volume = e.target.volume;
} else {
mediaVideoEl.volume = 0;
}
}
});
mediaVideoEl.addEventListener('volumechange', e => {
if(currentEntry.type === 'video' && config.theme.nativePlayer){
volume = e.target.volume;
localStorageSet('volume', volume);
}
});
playerEl.addEventListener('timeupdate', e => {
let buffer = 0.18;
if(!scrubberDragged){
if(currentEntry.duration && !currentEntry.needsDuration){
if(currentEntry.started){
let time = e.target.currentTime;
if(time > (currentEntry.previewEnd - currentEntry.audioStart) - buffer){
nextTrack(false);
} else {
let t = time / currentEntry.duration;
updatePreviewVolume(t);
updateScrubPosition(t);
}
} else {
}
} else {
let time = e.target.currentTime;
if(time > e.target.duration - buffer){
nextTrack(false);
}
}
}
});
playerEl.addEventListener('ended', e => {
if(!scrubberDragged){
nextTrack(false);
}
});
mediaVideoEl.addEventListener('ended', e => {
if(currentEntry.type === 'video' && config.theme.nativePlayer){
nextTrack(false);
}
});
bigButtonEl.addEventListener('click', e => {
playEntry(currentEntry);
});
volumeIconEl.addEventListener('click', e => {
toggleMute();
});
let volumeDragged = false;
volumeTrackContainerEl.addEventListener('mousedown', e => {
volumeDragged = true;
scrubVolume(e.clientX);
});
let volumeHovered = false;
volumeContainerEl.addEventListener('mouseenter', e => {
if(!scrubberDragged){
volumeHovered = true;
volumeContainerEl.classList.add('hover');
}
});
volumeContainerEl.addEventListener('mouseleave', e => {
volumeHovered = false;
if(!volumeDragged){
volumeContainerEl.classList.remove('hover');
}
});
function toggleMute(){
muted = !muted;
setVolume(volume, muted);
}
function scrubVolume(x){
if(volumeDragged){
let trackX = volumeTrackEl.getBoundingClientRect().x;
x -= trackX;
let scrubberWidth = 6;
let trackWidth = volumeTrackEl.clientWidth;
let t = 1 - Math.max(0, Math.min(1, (x - scrubberWidth/2) / (trackWidth - scrubberWidth)));
setVolume(t);
}
}
function endVolumeScrub(){
if(!volumeHovered){
volumeContainerEl.classList.remove('hover');
}
}
function setVolume(vol, mute=false){
volume = vol;
localStorageSet('volume', volume);
localStorageSet('muted', mute ? 1 : 0);
vol = mute ? 0 : vol;
if(currentEntry.type === 'video'){
if(config.theme.nativePlayer){
mediaVideoEl.volume = volume;
playerEl.volume = 0;
} else {
mediaVideoEl.volume = 0;
playerEl.volume = vol;
}
} else {
playerEl.volume = vol;
}
let scrubberWidth = 6;
let trackWidth = volumeTrackEl.clientWidth;
volumeFillEl.style.left = ((1-vol) * (trackWidth - scrubberWidth)) + 'px';
volumeIconEl.classList.remove(...volumeIcons);
if(vol <= 0){
volumeIconEl.classList.add(volumeIcons[0]);
} else if(vol <= 0.5){
volumeIconEl.classList.add(volumeIcons[1]);
} else {
volumeIconEl.classList.add(volumeIcons[2]);
}
}
let scrubberDragged = false;
scrubberTrackContainerEl.addEventListener('pointerdown', e => {
scrubberDragged = true;
currentEntry.started = true;
scrub(e.clientX);
});
document.addEventListener('pointerup', e => {
if(scrubberDragged){
scrubberDragged = false;
endScrub();
}
if(volumeDragged){
volumeDragged = false;
endVolumeScrub();
}
});
document.addEventListener('pointermove', e => {
scrub(e.clientX);
scrubVolume(e.clientX);
});
document.addEventListener('pointercancel', e => {
console.log(e);
});
nextTrackEl.addEventListener('click', e => {
nextTrack();
});
prevTrackEl.addEventListener('click', e => {
prevTrack();
});
loopSwitchEl.addEventListener('click', e => {
loopSwitch();
});
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){
if(currentEntry.needsDuration){
return;
}
//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 = ((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){
if(scrubberDragged){
let trackX = scrubberTrackEl.getBoundingClientRect().x;
x -= trackX;
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);
}
}
function endScrub(){
if(currentEntry.loading){
return;
}
playerEl.currentTime = scrubPosition * playerEl.duration;
if(currentEntry.type === 'video'){
mediaVideoEl.currentTime = scrubPosition * playerEl.duration;
}
}
function canPlay(entry){
return !entry.error && !entry.locked;
}
function nextTrack(manual=true){
if(!manual && loopMode === 'track'){
loadEntry(currentEntry);
} else if(!manual && currentEntry.remainingLoops > 0){
currentEntry.remainingLoops--;
updateLoops(currentEntry);
loadEntry(currentEntry);
} else {
let loop = manual || loopMode === "playlist";
let idx = media.indexOf(currentEntry)+1;
if(!loop && idx >= media.length){
return;
}
let entry = media[mod(idx, media.length)];
let i = 0;
while(!canPlay(entry) && i < media.length){
i++;
idx++;
if(!loop && idx >= media.length){
return;
}
entry = media[mod(idx, media.length)];
}
loadEntry(entry);
}
}
function prevTrack(){
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)];
let i = 0;
while(!canPlay(entry) && i < media.length){
i++;
idx--;
entry = media[mod(idx, media.length)];
}
loadEntry(entry);
}
}
function loopSwitch(mode){
loopSwitchEl.classList.remove('icon-loop', 'icon-loop-1');
loopSwitchEl.style.opacity = 1;
if(mode){
loopMode = mode;
} else {
if(loopMode === "none"){
loopMode = "playlist";
} else if(loopMode === "playlist"){
loopMode = "track";
} else {
loopMode = "none";
}
}
if(loopMode === "none"){
loopSwitchEl.classList.add('icon-loop');
loopSwitchEl.style.opacity = 0.5;
loopSwitchEl.setAttribute('title', 'loop off');
} else if(loopMode === "playlist"){
loopSwitchEl.classList.add('icon-loop');
loopSwitchEl.setAttribute('title', 'loop playlist');
} else {
loopSwitchEl.classList.add('icon-loop-1');
loopSwitchEl.setAttribute('title', 'loop track');
}
}
function setLoading(entry, delay=150){
if(entry.loading){
delay = 0;
}
if(entry === currentEntry){
bigButtonEl.classList.add('preloading');
setTimeout(() => {
if(bigButtonEl.classList.contains('preloading')){
bigButtonEl.classList.remove('preloading');
bigButtonEl.classList.add('loading');
}
}, delay);
}
entry.loading = true;
}
function removeLoading(entry, track=false){
if(entry === currentEntry){
bigButtonEl.classList.remove('loading', 'preloading');
}
if(track){
entry.trackEl.querySelector('button').classList.remove('loading', 'preloading');
}
entry.loading = false;
}
function updateLoops(entry){
if(entry.loopCount > 0){
let buttonEl = entry.trackEl.querySelector('button');
buttonEl.setAttribute('loopcount', entry.remainingLoops);
// buttonEl.setAttribute('title', 'loop count');
}
if(entry === currentEntry){
if(entry.loopCount > 0){
bigButtonEl.setAttribute('loopcount', entry.remainingLoops);
} else {
bigButtonEl.removeAttribute('loopcount');
}
}
}
function resetLoops(entry){
entry.remainingLoops = entry.loopCount;
updateLoops(entry);
}
function loadEntry(entry){
if(entry.error){
return;
}
if(entry.locked){
return;
}
if(entry === currentEntry){
if(entry.type === 'video'){
mediaVideoEl.currentTime = 0;
} else {
}
playerEl.currentTime = 0;
playerEl.play();
return;
}
currentEntry = entry;
media.forEach(thisEntry => {
thisEntry.trackEl.classList.remove('active');
thisEntry.trackEl.querySelector('button').classList.remove('pause');
if(thisEntry !== entry){
resetLoops(thisEntry);
}
updateLoops(entry);
});
if(!currentEntry.duration){
currentEntry.duration = 999;
currentEntry.needsDuration = true;
}
updateTrackPreview();
if(entry.preview){
scrubberTrackContainerEl.classList.add('preview');
updatePreviewVolume(0);
} else {
scrubberTrackContainerEl.classList.remove('preview');
}
if(entry.loopCount > 0){
bigButtonEl.classList.add('looped');
} else {
bigButtonEl.classList.remove('looped');
}
Array.from(mediaContainerEl.children).forEach(el => {
el.classList.remove('active');
});
if(entry.type === 'video'){
mediaVideoEl.src = entry.file;
mediaVideoEl.classList.add('active');
playerEl.classList.add('video');
if(config.theme.nativePlayer){
mediaVideoEl.volume = volume;
playerEl.volume = 0;
} else {
mediaVideoEl.volume = 0;
playerEl.volume = volume;
}
} else {
mediaImageEl.classList.add('active');
playerEl.classList.remove('video');
playerEl.volume = volume;
mediaVideoEl.pause();
mediaVideoEl.removeAttribute('src');
mediaVideoEl.load();
if(entry.cover){
loadCover(entry.cover);
} else {
loadCover(config.cover);
}
}
playerEl.src = entry.file;
entry.started = false;
setLoading(currentEntry);
entry.trackEl.classList.add('active');
controlsEl.querySelector('.title').innerHTML = entry.title;
if(entry.info && (config.theme.infoStyle !== 'overlaid-toggle' || entry.infoToggled)){
mediaInfoEl.innerHTML = entry.info;
mediaInfoContainer.classList.add('active');
} else {
mediaInfoEl.innerHTML = '';
mediaInfoContainer.classList.remove('active');
}
}
function playEntry(entry){
if(currentEntry === entry){
if(entry.loading){
return;
}
if(playerEl.paused){
playerEl.play();
entry.trackEl.querySelector('button').classList.add('pause');
} else {
playerEl.pause();
}
} else {
loadEntry(entry);
}
}
function toHMS(secs){
if(isNaN(secs)){
return '00:00';
}
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;
return [hours,minutes,seconds]
.map(v => v < 10 ? "0" + v : v)
.filter((v,i) => v !== "00" || i > 0)
.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);
}
</script>
</html>