life-diary
Version:
Life Diary ❤️ your albums, your journey, your data
394 lines (360 loc) • 11.1 kB
HTML
<style>
footer[is="ld-fullscreen"] {
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
background-color: black;
}
footer[is="ld-fullscreen"].details div {
opacity: 1;
}
footer[is="ld-fullscreen"].live > button {
display: block;
}
footer[is="ld-fullscreen"].map img,
footer[is="ld-fullscreen"].map video {
z-index: 999;
position: absolute;
bottom: 16px;
left: 16px;
max-width: 30%;
max-height: 50%;
}
footer[is="ld-fullscreen"].fallback {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 999;
}
body.ld-fullscreen > *:not(footer[is="ld-fullscreen"]) {
visibility: hidden;
}
</style>
<style scoped>
menu {
display: none;
}
div, img, video {
max-width: 100%;
max-height: 100%;
max-width: 100vw;
max-height: 100vh;
}
div.details {
position: absolute;
bottom: 0;
width: 100%;
padding: 16px;
color: white;
background: rgba(0, 0, 0, .7);
opacity: 0;
}
div[ref="map"] {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
}
h2 {
margin: 0;
}
h2, p {
white-space: pre-line;
}
button[ref="geo"] {
position: fixed;
top: 8px;
right: 8px;
padding: 16px;
font-size: 1.2rem;
background: none;
border: none;
cursor: pointer;
opacity: 0;
}
button[ref="geo"]:focus, button[ref="geo"]:hover {
opacity: 1;
}
img, div.details, div[ref="map"], button[ref="geo"] {
transition: opacity 0.3s ease-in;
}
button.fallback {
position: fixed;
padding: 16px;
top: 4px;
left: 4px;
z-index: 9999;
}
.contextmenu {
min-width: 100px;
background: white;
border: 1px solid rgba(0, 0, 0, .3);
border-radius: 4px;
}
.contextmenu > .item {
padding: 8px;
cursor: pointer;
transition: all 0.3s ease-in;
}
.contextmenu > .item:hover {
transform: scale(1.1) 0.3s ease-in;
}
</style>
<footer is="ld-fullscreen"></footer>
<script type="module">
/**
* ISC License
*
* Copyright (c) 2020, Andrea Giammarchi, @WebReflection
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
* OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
*/
import {render, html, ref} from '@uce';
import IMAGE from '/js/image.js';
import EXIF from '/js/exif.js';
import Leaflet from '/js/leaflet.js';
import contextmenu from '/js/contextmenu.js';
import swiped from '/js/swiped.js';
const DELAY = 4000;
const {stringify, parse} = JSON;
let fallback = false;
const ontransitionend = ({currentTarget}) => {
if (currentTarget.style.opacity == 0)
currentTarget.style.display = 'none';
};
const view = (element, file, media, showMap) => {
const {title, description, feature} = file;
const {properties: {emojiFlag, nameEn}} = (feature || {properties:{}});
const small = `${emojiFlag || ''} ${nameEn || ''}`;
render(element, html.for(element, 'view')`
<div ref="swiper">${media}</div>
<div class="details">
<h2>${title || ''}</h2>
<p>${description || ''}</p>
<small>${small}</small>
</div>
<div ref="map" .data=${file} ontransitionend=${ontransitionend} />
<button ref="geo" onclick=${showMap}>🗺️</button>
`)
};
const hideDetails = element => {
element.classList.remove('details');
};
const fullscreenchange = () => {
dispatchEvent(new CustomEvent('fullscreenchange'));
};
const onmousemove = ({currentTarget}) => {
const {style} = currentTarget.querySelector('button[ref="geo"]');
if (style.opacity != 1) {
style.opacity = 1;
setTimeout(() => { style.opacity = null; }, DELAY);
}
};
const requestFullscreen = element => {
if (element.requestFullscreen)
return element.requestFullscreen();
return new Promise($ => {
if (element.webkitRequestFullscreen)
$(element.webkitRequestFullscreen());
else {
// ... thank you iOS Safari ...
element.addEventListener('dblclick', fullscreenchange);
document.body.classList.add('ld-fullscreen');
$(element.classList.add('fallback'));
}
});
};
const getCoords = ({coords, metadata}) => (
(coords && coords.length) ?
coords :
(metadata && metadata.EXIF ? EXIF.coords(metadata.EXIF) : [])
);
const mapSetup = (map, coords) => new Promise($ => {
map.style.visibility = 'visible';
Leaflet.setup(map);
Leaflet.show(coords);
});
const displayAndShow = el => {
el.style.display = 'block';
requestAnimationFrame(() => requestAnimationFrame(() => {
el.style.opacity = 1;
}));
};
const instrumentedMap = new WeakMap;
export default {
fullscreen(files, i) {
requestFullscreen(this).then(() => {
this.classList.add('live');
const moveRight = () => {
i = ++i < files.length ? i : 0;
};
const moveLeft = () => {
i = --i > -1 ? i : files.length + i;
};
const showFile = index => {
if (index !== i) {
ref(this).geo.blur();
showContent(files[i]);
}
};
const showMap = event => {
event.preventDefault();
event.stopImmediatePropagation();
const {map, geo} = ref(this);
const coords = getCoords(files[i]);
if (map.style.opacity == 1) {
this.classList.remove('map');
geo.title = 'Open Map';
map.style.opacity = 0;
}
else {
this.classList.add('map');
geo.title = 'Close Map';
displayAndShow(map);
mapSetup(map, coords);
};
};
const showContent = file => {
const {full, title} = file;
let media;
if (IMAGE.test(full)) {
const onload = ({currentTarget: {style}}) => {
style.opacity = 1;
showDetails(file);
};
media = html.for(file)`
<img style="opacity:0"
onload=${onload} title=${title} src=${full}>
`;
}
else
media = html.for(file)`<video controls src=${full} />`;
view(this, file, media, showMap);
showDetails(file);
const {map, geo} = ref(this);
const coords = getCoords(file);
mapSetup(map, coords);
};
let timer = 0;
const showDetails = ({title, description, feature}) => {
clearTimeout(timer);
if (title || description || feature) {
requestAnimationFrame(() => {
this.classList.add('details');
});
timer = setTimeout(hideDetails, DELAY, this);
}
else {
timer = 0;
hideDetails(this);
}
};
const onkeydown = event => {
let index = i;
switch (event.key) {
case 'ArrowRight':
moveRight();
break;
case 'ArrowLeft':
moveLeft();
break;
}
showFile(index);
};
const onfullscreenchange = () => {
const {ownerDocument} = this;
if (!ownerDocument.fullscreenElement && !ownerDocument.webkitFullscreenElement) {
document.body.classList.remove('ld-fullscreen');
const {map, geo} = ref(this);
map.style.opacity = 0;
this.className = '';
view(this, {title: '', description: ''}, '', null);
this.removeEventListener('mousemove', onmousemove);
this.removeEventListener('pointerdown', onmousemove);
removeEventListener('keydown', onkeydown);
removeEventListener('fullscreenchange', onfullscreenchange);
removeEventListener('webkitfullscreenchange', onfullscreenchange);
setTimeout(() => map.style.display = 'none', 3);
}
};
this.addEventListener('mousemove', onmousemove);
this.addEventListener('pointerdown', onmousemove);
addEventListener('keydown', onkeydown);
addEventListener('fullscreenchange', onfullscreenchange);
addEventListener('webkitfullscreenchange', onfullscreenchange);
showContent(files[i]);
// avoid map interfeering with the gallery
const {map, swiper} = ref(this);
map.style.display = 'block';
map.style.visibility = 'hidden';
if (!instrumentedMap.has(map)) {
instrumentedMap.set(map, true);
swiped(swiper, {
left: () => {
let index = i;
moveRight();
showFile(index);
},
right: () => {
let index = i;
moveLeft();
showFile(index);
}
});
contextmenu(map, [
{
text: '🗺️ Update Geo Location',
action: event => {
const {clientX, clientY} = event;
const {map} = ref(this);
const {data} = map;
const [GPSLatitude, GPSLongitude] = Leaflet.mouseCoords;
const GPSLatitudeRef = GPSLatitude < 0 ? 'South' : 'North';
const GPSLongitudeRef = GPSLongitude < 0 ? 'West' : 'East';
const info = {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: stringify({
coords: {
GPSLatitude, GPSLatitudeRef,
GPSLongitude, GPSLongitudeRef
}
})
};
fetch(data.full, info).then(b => b.text()).then(result => {
if (result === 'NO')
alert(`Unable to updated Geo location`);
else {
data.coords = [GPSLatitude, GPSLongitude];
mapSetup(map, data.coords);
if (result)
data.feature = parse(result);
}
});
}
}
]);
}
});
}
};
</script>