UNPKG

life-diary

Version:

Life Diary ❤️ your albums, your journey, your data

394 lines (360 loc) 11.1 kB
<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>