404 lines
8.9 KiB
HTML
Executable File
404 lines
8.9 KiB
HTML
Executable File
<html>
|
|
<script>'<base href="BASEPATH">';</script>
|
|
<body>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.face {
|
|
position: absolute;
|
|
display: inline-block;
|
|
border: 1px solid rgb(128,0,0);
|
|
border-radius: 0.5em;
|
|
opacity: 0.5;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.face:hover {
|
|
border-color: #ff0000;
|
|
background-color: rgba(128,0,0,0.5);
|
|
}
|
|
|
|
#photo {
|
|
position: fixed;
|
|
display: inline-block;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
z-index: 0;
|
|
background-repeat: no-repeat;
|
|
background-position: 50% 50%;
|
|
background-size: contain;
|
|
background-color: #222;
|
|
}
|
|
|
|
#placeholder {
|
|
position: absolute;
|
|
left: -1000px;
|
|
display: none;
|
|
}
|
|
|
|
#footer {
|
|
position: absolute;
|
|
bottom: 0px;
|
|
left: 0px;
|
|
right: 0px;
|
|
opacity: 0.6;
|
|
background: linear-gradient(45deg, rgba(16, 16, 16, 1), transparent);
|
|
color: white;
|
|
font-size: 1.5em;
|
|
line-height: 1.5em;
|
|
font-family: Arial;
|
|
box-shadow: 0px -0.25em 1em black;
|
|
text-shadow: 0 1px 0 black;
|
|
z-index: 100;
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
#info {
|
|
padding: 0.5em 1em;
|
|
}
|
|
|
|
#loading {
|
|
padding: 0.5em 1em;
|
|
}
|
|
|
|
</style>
|
|
<img id="placeholder"></img>
|
|
<div id="photo">
|
|
</div>
|
|
<div id="footer">
|
|
<div id="info">Loading photoset...</div>
|
|
<div id="loading"></div>
|
|
</div>
|
|
<script>
|
|
|
|
let base,
|
|
placeholder,
|
|
info,
|
|
photos = [],
|
|
photoIndex = -1;
|
|
|
|
let mode, filter;
|
|
|
|
const days = [
|
|
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
|
|
"Saturday"
|
|
];
|
|
const months = [
|
|
"January", "February", "March", "April", "May", "June",
|
|
"July", "August", "September", "October", "November", "December"
|
|
];
|
|
|
|
let activeFaces = [];
|
|
|
|
let paused = false,
|
|
tap = 0;
|
|
let countdown = 15;
|
|
let scheduled = false;
|
|
|
|
|
|
const onClick = (e) => {
|
|
const now = new Date().getTime();
|
|
if (tap && (now - tap < 300)) {
|
|
toggleFullscreen();
|
|
tap = 0;
|
|
} else {
|
|
tap = new Date().getTime();
|
|
}
|
|
};
|
|
|
|
const shuffle = (arr) => {
|
|
var index = arr.length, tmp, random;
|
|
while (index) {
|
|
random = Math.floor(Math.random() * index);
|
|
index--;
|
|
tmp = arr[index];
|
|
arr[index] = arr[random];
|
|
arr[random] = tmp;
|
|
}
|
|
return arr;
|
|
};
|
|
|
|
const faceClick = (event, face) => {
|
|
console.log(face);
|
|
face.relatedFaces.forEach((photo) => {
|
|
window.open(base + photo.path);
|
|
});
|
|
event.preventDefault = true;
|
|
event.stopImmediatePropagation();
|
|
event.stopPropagation();
|
|
return false;
|
|
};
|
|
|
|
const makeFaceBoxes = () => {
|
|
Array.prototype.forEach.call(document.querySelectorAll('.face'), (el) => {
|
|
el.parentElement.removeChild(el);
|
|
});
|
|
|
|
if (activeFaces.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const el = document.getElementById("photo"),
|
|
photo = photos[photoIndex];
|
|
|
|
let width, height, offsetLeft = 0, offsetTop = 0;
|
|
|
|
/* If photo is wider than viewport, it will be 100% width and < 100% height */
|
|
if (photo.width / photo.height > el.offsetWidth / el.offsetHeight) {
|
|
width = 100;
|
|
height = 100 * photo.height / photo.width * el.offsetWidth / el.offsetHeight;
|
|
offsetLeft = 0;
|
|
offsetTop = (100 - height) * 0.5;
|
|
} else {
|
|
width = 100 * photo.width / photo.height * el.offsetHeight / el.offsetWidth;
|
|
height = 100;
|
|
offsetLeft = (100 - width) * 0.5;
|
|
offsetTop = 0;
|
|
}
|
|
|
|
activeFaces.forEach((face) => {
|
|
const box = document.createElement("div");
|
|
box.classList.add("face");
|
|
document.body.appendChild(box);
|
|
box.style.left = offsetLeft + Math.floor(face.left * width) + "%";
|
|
box.style.top = offsetTop + Math.floor(face.top * height) + "%";
|
|
box.style.width = Math.floor((face.right - face.left) * width) + "%";
|
|
box.style.height = Math.floor((face.bottom - face.top) * height) + "%";
|
|
box.addEventListener("click", (e) => { return faceClick(e, face); });
|
|
});
|
|
};
|
|
|
|
const loadPhoto = (index) => {
|
|
const photo = photos[index],
|
|
xml = new XMLHttpRequest(),
|
|
url = base + photo.path + "thumbs/scaled/" + photo.filename,
|
|
taken = new Date(photo.taken);
|
|
|
|
document.getElementById("loading").textContent = "0%";
|
|
|
|
xml.onprogress = function (event) {
|
|
var alpha = 0;
|
|
if (event.total) {
|
|
alpha = event.loaded / event.total;
|
|
document.getElementById("loading").textContent = Math.ceil(100 * alpha) + "%";
|
|
} else {
|
|
document.getElementById("loading").textContent = "0%";
|
|
}
|
|
};
|
|
|
|
xml.onload = async (event) => {
|
|
info.textContent =
|
|
days[taken.getDay()] + ", " +
|
|
months[taken.getMonth()] + " " +
|
|
taken.getDate() + " " +
|
|
taken.getFullYear();
|
|
activeFaces = [];
|
|
makeFaceBoxes();
|
|
document.getElementById("photo").style.backgroundImage =
|
|
`url(${encodeURI(url).replace(/\(/g, '%28').replace(/\)/g, '%29')})`;
|
|
countdown = 15;
|
|
tick();
|
|
|
|
try {
|
|
const res = await window.fetch("api/v1/photos/faces/" + photo.id);
|
|
const faces = await res.json();
|
|
activeFaces = faces;
|
|
makeFaceBoxes(photo);
|
|
} catch (error) {
|
|
console.error(error);
|
|
info.textContent += "Unable to obtain face information :(";
|
|
}
|
|
};
|
|
|
|
xml.onerror = function(event) {
|
|
info.textContent = "Error loading photo. Trying next photo.";
|
|
nextPhoto();
|
|
}
|
|
|
|
xml.open("GET", url, true);
|
|
xml.send();
|
|
}
|
|
|
|
const prevPhoto = async () => {
|
|
if (photoIndex > 0) {
|
|
photoIndex--;
|
|
loadPhoto(photoIndex);
|
|
return;
|
|
}
|
|
|
|
if (mode !== 'random/') {
|
|
photoIndex = photos.length;
|
|
loadPhoto(photoIndex);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await window.fetch(
|
|
`api/v1/photos/${mode}${filter.replace(/ +/g, "%20")}`);
|
|
const data = await res.json();
|
|
if (data && data.items) {
|
|
info.textContent = photos.length + " photos found. Shuffling.";
|
|
photos = shuffle(data.items);
|
|
photoIndex = (photoIndex + 1) % photos.length;
|
|
loadPhoto(photoIndex);
|
|
} else if (data) {
|
|
photos.push(data);
|
|
photoIndex = photos.length - 1;
|
|
loadPhoto(photoIndex);
|
|
} else {
|
|
info.textContent = "No photos found for " + filter + ".";
|
|
}
|
|
} catch(error) {
|
|
console.error(error);
|
|
info.textContent = "Unable to fetch " + mode + "=" + filter + " :(";
|
|
}
|
|
}
|
|
|
|
const nextPhoto = async () => {
|
|
if (photoIndex < photos.length - 1) {
|
|
photoIndex++;
|
|
loadPhoto(photoIndex);
|
|
return;
|
|
}
|
|
|
|
if (mode !== 'random/') {
|
|
photoIndex = 0;
|
|
loadPhoto(photoIndex);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await window.fetch(
|
|
`api/v1/photos/${mode}${filter.replace(/ +/g, "%20")}`);
|
|
const data = await res.json();
|
|
if (data && data.items) {
|
|
info.textContent = photos.length + " photos found. Shuffling.";
|
|
photos = shuffle(data.items);
|
|
photoIndex = 0;
|
|
loadPhoto(photoIndex);
|
|
} else if (data) {
|
|
photos.push(data);
|
|
photoIndex = photos.length - 1;
|
|
loadPhoto(photoIndex);
|
|
} else {
|
|
info.textContent = "No photos found for " + filter + ".";
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
info.textContent = "Unable to fetch " + mode + "=" + filter + " :(";
|
|
}
|
|
}
|
|
|
|
const tick = () => {
|
|
if (scheduled) {
|
|
clearTimeout(scheduled);
|
|
}
|
|
document.getElementById("loading").textContent = countdown + "s";
|
|
if (countdown > 0) {
|
|
/* If there is a timer running, then decrement the counter */
|
|
if (scheduled) {
|
|
countdown--;
|
|
}
|
|
scheduled = setTimeout(tick, 1000);
|
|
} else {
|
|
scheduled = null;
|
|
countdown = 15;
|
|
nextPhoto();
|
|
}
|
|
};
|
|
|
|
const schedule = () => {
|
|
if (scheduled) {
|
|
clearTimeout(scheduled);
|
|
}
|
|
tick();
|
|
};
|
|
|
|
const toggleFullscreen = () => {
|
|
if (!document.fullscreenElement) {
|
|
document.documentElement.requestFullscreen();
|
|
} else {
|
|
if (document.exitFullscreen) {
|
|
document.exitFullscreen();
|
|
}
|
|
}
|
|
};
|
|
|
|
const onKeyDown = (event) => {
|
|
switch (event.keyCode) {
|
|
case 32: /* space */
|
|
paused = !paused;
|
|
if (!paused) {
|
|
tick();
|
|
} else {
|
|
document.getElementById("loading").textContent = "||";
|
|
clearTimeout(scheduled);
|
|
scheduled = null;
|
|
}
|
|
return;
|
|
|
|
case 37: /* left */
|
|
prevPhoto();
|
|
return;
|
|
|
|
case 39: /* right */
|
|
nextPhoto();
|
|
return;
|
|
|
|
case 13: /* enter */
|
|
toggleFullscreen();
|
|
return;
|
|
}
|
|
};
|
|
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
var tmp = document.querySelector("base");
|
|
if (tmp) {
|
|
/* Make sure there is a trailing slash */
|
|
base = new URL(tmp.href).pathname.replace(/\/$/, "") + "/";
|
|
} else {
|
|
base = "/";
|
|
}
|
|
|
|
var timeout = 0;
|
|
window.addEventListener("resize", (event) => {
|
|
if (timeout) {
|
|
window.clearTimeout(timeout);
|
|
}
|
|
timeout = window.setTimeout(makeFaceBoxes, 250);
|
|
});
|
|
|
|
document.addEventListener("click", onClick);
|
|
document.addEventListener("keydown", onKeyDown);
|
|
|
|
info = document.getElementById("info");
|
|
|
|
/* Trim off everything up to and including the ? (if its there) */
|
|
var parts = window.location.href.match(/^.*\?(.*)$/);
|
|
if (parts) {
|
|
parts = parts[1].split("=");
|
|
if (parts.length == 1) {
|
|
mode = "holiday/";
|
|
filter = parts[0];
|
|
} else {
|
|
mode = parts[0].replace(/\/*$/, '/');
|
|
filter = parts[1];
|
|
}
|
|
mode = mode
|
|
} else {
|
|
mode = "random/"
|
|
filter = "";
|
|
}
|
|
|
|
nextPhoto();
|
|
});
|
|
</script>
|
|
</body>
|