Added lightbox
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
parent
7330f5eb5d
commit
5f9fbe21fb
@ -47,7 +47,8 @@
|
||||
"iron-location": "PolymerElements/iron-location#^1.0.0",
|
||||
"iron-a11y-keys": "PolymerElements/iron-a11y-keys#^1.0.0",
|
||||
"iron-a11y-keys-behavior": "PolymerElements/iron-a11y-keys-behavior#^1.0.0",
|
||||
"es6-shim": "^0.35.3"
|
||||
"es6-shim": "^0.35.3",
|
||||
"paper-radio-group": "PolymerElements/paper-radio-group#^2.2.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"polymer": "^1.4.0",
|
||||
|
193
frontend/elements/photo-lightbox.html
Normal file
193
frontend/elements/photo-lightbox.html
Normal file
@ -0,0 +1,193 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||
<link rel="import" href="../bower_components/iron-icon/iron-icon.html">
|
||||
<link rel="import" href="../bower_components/iron-icons/iron-icons.html">
|
||||
<link rel="import" href="../bower_components/iron-pages/iron-pages.html">
|
||||
</head>
|
||||
|
||||
<dom-module id="photo-lightbox">
|
||||
<template>
|
||||
<style is="custom-style" include="iron-flex iron-flex-alignment iron-positioning">
|
||||
:host {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
-webkit-transition: opacity 0.5s ease-in-out;
|
||||
border-width: 5px;
|
||||
box-sizing: border-box;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#image {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: contain;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
-webkit-transition: opacity 0.5s ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="image"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
Polymer({
|
||||
is: "photo-lightbox",
|
||||
properties: {
|
||||
"src": {
|
||||
type: String
|
||||
},
|
||||
"thumb": {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
|
||||
observers: [
|
||||
"srcChanged(src)"
|
||||
],
|
||||
|
||||
listeners: {
|
||||
"keydown": "onKeyDown",
|
||||
"tap": "onTap",
|
||||
"blur": "onBlur",
|
||||
"focus": "onFocus",
|
||||
"scroll": "onScroll"
|
||||
},
|
||||
|
||||
onFocus: function() {
|
||||
this.hasFocus = true;
|
||||
},
|
||||
|
||||
onBlur: function() {
|
||||
this.hasFocus = false;
|
||||
},
|
||||
|
||||
next: function() {
|
||||
this.fire("next");
|
||||
},
|
||||
|
||||
previous: function() {
|
||||
this.fire("previous");
|
||||
},
|
||||
|
||||
onKeyDown: function(e) {
|
||||
if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.keyCode) {
|
||||
case 39: /* right */
|
||||
if (this.hasFocus) {
|
||||
this.next();
|
||||
}
|
||||
break;
|
||||
case 37: /* left */
|
||||
if (this.hasFocus) {
|
||||
this.previous();
|
||||
}
|
||||
break;
|
||||
case 27: /* escape */
|
||||
this.hasFocus = false;
|
||||
this.close();
|
||||
break;
|
||||
default:
|
||||
console.log(e.keyCode);
|
||||
break;
|
||||
}
|
||||
if (this.hasFocus) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this.style.opacity = 0;
|
||||
|
||||
this.async(function() {
|
||||
this.style.display = 'none';
|
||||
this.image = undefined;
|
||||
this.$.image.style.opacity = 0;
|
||||
this.$.image.style.removeProperty('background-image');
|
||||
this.closed = true;
|
||||
this.opened = false;
|
||||
this.fire("close");
|
||||
}, 250);
|
||||
},
|
||||
|
||||
srcChanged: function(src) {
|
||||
this.loadImage(src);
|
||||
},
|
||||
|
||||
onTap: function(event) {
|
||||
if (!this.style.display || this.style.display == "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.detail.x <= 0.1 * this.offsetWidth) {
|
||||
this.previous();
|
||||
} else if (event.detail.x >= 0.9 * this.offsetWidth) {
|
||||
this.next();
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
open: function() {
|
||||
this.closed = false;
|
||||
this.opened = true;
|
||||
this.style.opacity = 1;
|
||||
this.style.borderColor = 'blue';
|
||||
this.style.display = 'block';
|
||||
this.focus();
|
||||
},
|
||||
|
||||
loadImage: function(image) {
|
||||
if (this.$.image.style.opacity != 0) {
|
||||
this.waitUntil = (Date.now() / 1000) + 500;
|
||||
} else {
|
||||
this.waitUntil = 0;
|
||||
}
|
||||
this.$.image.style.opacity = 0;
|
||||
this.style.borderColor = 'orange';
|
||||
|
||||
this.image = new Image();
|
||||
this.loading = image;
|
||||
|
||||
this.image.onload = function(src) {
|
||||
var remaining = Math.max(this.waitUntil - Date.now() / 1000, 0);
|
||||
this.async(function() {
|
||||
if (!this.image || this.loading != src) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.image.style.backgroundImage = 'url("' + this.image.src + '")';
|
||||
this.$.image.style.opacity = 1;
|
||||
this.style.borderColor = 'black';
|
||||
this.image = undefined;
|
||||
}, remaining);
|
||||
}.bind(this, image);
|
||||
|
||||
this.image.src = image;
|
||||
},
|
||||
|
||||
attached: function() {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
||||
</html>
|
@ -17,11 +17,14 @@
|
||||
<link rel="import" href="../../bower_components/paper-dialog-scrollable/paper-dialog-scrollable.html">
|
||||
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
|
||||
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
|
||||
<link rel="import" href="../../bower_components/paper-radio-group/paper-radio-group.html">
|
||||
<link rel="import" href="../../bower_components/paper-radio-button/paper-radio-button.html">
|
||||
<link rel="import" href="../../bower_components/paper-toast/paper-toast.html">
|
||||
<link rel="import" href="../../bower_components/iron-resizable-behavior/iron-resizable-behavior.html">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" />
|
||||
|
||||
<link rel="import" href="../../elements/photo-lightbox.html">
|
||||
<link rel="import" href="../../elements/photo-thumbnail.html">
|
||||
|
||||
<script src="fetch.js"></script>
|
||||
@ -68,17 +71,35 @@
|
||||
left: -1000px;
|
||||
}
|
||||
|
||||
app-header {
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
app-header-layout {
|
||||
--layout-fit: {
|
||||
overflow-y: hidden !important;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
.date-line {
|
||||
display: block;
|
||||
padding: 0.5em 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#content {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
photo-thumbnail {
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
photo-thumbnail[selected] {
|
||||
border-color: blue;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<app-location route="{{route}}"></app-location>
|
||||
@ -86,13 +107,16 @@
|
||||
<photo-thumbnail id="placeholder"></photo-thumbnail>
|
||||
|
||||
<app-header-layout fullbleed has-scrolling-region>
|
||||
<app-header slot="header" fixed>
|
||||
<app-header slot="header">
|
||||
<paper-spinner active$="[[loading]]" class="thin"></paper-spinner>
|
||||
<paper-radio-group selected="{{order}}">
|
||||
<paper-radio-button name="by-date">By date</paper-radio-button>
|
||||
<paper-radio-button name="by-album">By album</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
<div>[[path]]</div>
|
||||
</app-header>
|
||||
<div id="content">
|
||||
<div id="thumbnails" class="thumbnails layout horizontal wrap">
|
||||
</div>
|
||||
<div id="thumbnails" class="thumbnails layout horizontal wrap"></div>
|
||||
<div id="magic"></div>
|
||||
<div class="layout horizontal">
|
||||
<paper-button disabled$="[[!prev]]" on-tap="loadPrevPhotos">prev</paper-button>
|
||||
@ -106,6 +130,7 @@
|
||||
</div>
|
||||
</app-header-layout>
|
||||
<paper-toast id="toast"></paper-toast>
|
||||
<photo-lightbox tabindex="0" id="lightbox" on-close="lightBoxClose" on-next="lightBoxNext" on-previous="lightBoxPrevious"></photo-lightbox>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -114,6 +139,10 @@
|
||||
Polymer({
|
||||
is: "ketr-photos",
|
||||
properties: {
|
||||
order: {
|
||||
type: String,
|
||||
value: "by-date"
|
||||
},
|
||||
"loading": Boolean,
|
||||
"photos": Array,
|
||||
prev: {
|
||||
@ -130,6 +159,65 @@
|
||||
"widthChanged(calcWidth)"
|
||||
],
|
||||
|
||||
listeners: {
|
||||
"scroll": "onScroll",
|
||||
"iron-resize" : "onResize"
|
||||
},
|
||||
|
||||
onScroll: function(event) {
|
||||
if (this.disableScrolling) {
|
||||
event.preventDefault();
|
||||
window.scrollTo(this.topStickX, this.topStickY);
|
||||
}
|
||||
},
|
||||
|
||||
findPhoto: function(photo) {
|
||||
var photos = this.$.thumbnails.querySelectorAll("photo-thumbnail");
|
||||
for (var i = 0; i < photos.length; i++) {
|
||||
if (photos[i] == photo) {
|
||||
return { index: i, photos: photos };
|
||||
}
|
||||
}
|
||||
return { index: -1, photos: photos };
|
||||
},
|
||||
|
||||
lightBoxClose: function(event) {
|
||||
this.disableScrolling = false;
|
||||
},
|
||||
|
||||
lightBoxNext: function(event) {
|
||||
var results = this.findPhoto(this.lightBoxElement);
|
||||
|
||||
/* If there are less than 2 rows less (2 * cols) then queue up more to load! */
|
||||
if (results.index + (this.cols * 2) >= results.photos.length && this.next) {
|
||||
this.loadNextPhotos();
|
||||
}
|
||||
|
||||
if (results.index == -1 || results.index + 1 >= results.photos.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var photo = results.photos[results.index + 1];
|
||||
photo.scrollIntoView(false);
|
||||
this.topStickX = window.scrollX;
|
||||
this.topStickY = window.scrollY;
|
||||
this.loadLightbox(photo);
|
||||
},
|
||||
|
||||
|
||||
lightBoxPrevious: function(event) {
|
||||
var results = this.findPhoto(this.lightBoxElement);
|
||||
if (results.index == -1 || results.index < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var photo = results.photos[results.index - 1];
|
||||
photo.scrollIntoView(false);
|
||||
this.topStickX = window.scrollX;
|
||||
this.topStickY = window.scrollY;
|
||||
this.loadLightbox(photo);
|
||||
},
|
||||
|
||||
widthChanged: function(calcWidth) {
|
||||
var thumbs = this.$.thumbnails.querySelectorAll("photo-thumbnail");
|
||||
Array.prototype.forEach.call(thumbs, function(thumb) {
|
||||
@ -142,17 +230,27 @@
|
||||
Polymer.IronResizableBehavior
|
||||
],
|
||||
|
||||
listeners: {
|
||||
"iron-resize" : "onResize"
|
||||
},
|
||||
|
||||
date: function(item) {
|
||||
var datetime = item.taken || item.modified || item.added;
|
||||
return datetime.replace(/T.*$/, "");
|
||||
},
|
||||
|
||||
loadLightbox: function(el) {
|
||||
if (this.lightBoxElement) {
|
||||
this.lightBoxElement.removeAttribute("selected");
|
||||
}
|
||||
el.setAttribute("selected", true);
|
||||
|
||||
this.$.lightbox.src = this.base + el.item.path + "/" + el.item.filename;
|
||||
this.lightBoxElement = el;
|
||||
this.disableScrolling = true;
|
||||
this.topStickX = window.scrollX;
|
||||
this.topStickY = window.scrollY;
|
||||
this.$.lightbox.open();
|
||||
},
|
||||
|
||||
_imageTap: function(event) {
|
||||
window.open(this.base + event.model.item.path + "/" + event.model.item.filename, "image");
|
||||
this.loadLightbox(event.currentTarget);
|
||||
},
|
||||
|
||||
_pathTap: function(event) {
|
||||
@ -161,12 +259,12 @@
|
||||
|
||||
loadNextPhotos: function() {
|
||||
var cursor = this.photos[this.photos.length - 1];
|
||||
this._loadPhotos(cursor.id + "_" + cursor.taken.toString().replace(/T.*/, ""), +1, true);
|
||||
this._loadPhotos(cursor.taken.toString().replace(/T.*/, "") + "_" + cursor.id, +1, true);
|
||||
},
|
||||
|
||||
loadPrevPhotos: function() {
|
||||
var cursor = this.photos[0];
|
||||
this._loadPhotos(cursor.id + "_" + cursor.taken.toString().replace(/T.*/, ""), -1);
|
||||
this._loadPhotos(cursor.taken.toString().replace(/T.*/, "") + "_" + cursor.id, -1);
|
||||
},
|
||||
|
||||
_loadPhotos: function(start, dir, append) {
|
||||
@ -226,13 +324,14 @@
|
||||
thumbnail.item = photo;
|
||||
thumbnail.width = this.calcWidth;
|
||||
thumbnail.addEventListener("click", this._imageTap.bind(this));
|
||||
var datetime = (photo.taken || photo.modified || photo.added).replace(/T.*$/, "");
|
||||
if (this.lastDate != datetime) {
|
||||
var dateLine = document.createElement("div");
|
||||
dateLine.classList.add("date-line");
|
||||
dateLine.textContent = datetime;
|
||||
this.lastDate = datetime;
|
||||
Polymer.dom(this.$.thumbnails).appendChild(dateLine);
|
||||
var datetime = (photo.taken || photo.modified || photo.added).replace(/T.*$/, ""),
|
||||
dateBlock = this.querySelector("#date-" + datetime);
|
||||
if (!dateBlock) {
|
||||
dateBlock = document.createElement("div");
|
||||
dateBlock.id = "date-" + datetime;
|
||||
dateBlock.classList.add("date-line");
|
||||
dateBlock.textContent = datetime;
|
||||
Polymer.dom(this.$.thumbnails).appendChild(dateBlock);
|
||||
}
|
||||
|
||||
Polymer.dom(this.$.thumbnails).appendChild(thumbnail);
|
||||
@ -257,9 +356,11 @@
|
||||
|
||||
onResize: function(event) {
|
||||
this.debounce("resize", function() {
|
||||
var width = Math.max(this.$.placeholder.offsetWidth || 0, 200),
|
||||
cols = Math.floor(this.clientWidth / width),
|
||||
calc = width + Math.floor((this.clientWidth % width) / cols);
|
||||
var width = Math.max(this.$.placeholder.offsetWidth || 0, 200);
|
||||
|
||||
this.cols = Math.floor(this.clientWidth / width);
|
||||
|
||||
var calc = width + Math.floor((this.clientWidth % width) / this.cols);
|
||||
if (calc != this.calcWidth) {
|
||||
this.calcWidth = calc;
|
||||
}
|
||||
@ -303,6 +404,8 @@
|
||||
this._loadPhotos();
|
||||
|
||||
this.onResize();
|
||||
|
||||
document.addEventListener("scroll", this.onScroll.bind(this));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -63,6 +63,7 @@ app.use(function(req, res, next){
|
||||
});
|
||||
|
||||
app.use(basePath + "api/v1/photos", require("./routes/photos"));
|
||||
app.use(basePath + "api/v1/days", require("./routes/days"));
|
||||
|
||||
/* Declare the "catch all" index route last; the final route is a 404 dynamic router */
|
||||
app.use(basePath, require("./routes/index"));
|
||||
|
49
server/routes/days.js
Normal file
49
server/routes/days.js
Normal file
@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
|
||||
const express = require("express"),
|
||||
fs = require("fs"),
|
||||
url = require("url"),
|
||||
config = require("config"),
|
||||
moment = require("moment");
|
||||
|
||||
let photoDB;
|
||||
|
||||
require("../db").then(function(db) {
|
||||
photoDB = db;
|
||||
});
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/* Each photos has:
|
||||
|
||||
* locations
|
||||
* people
|
||||
* date
|
||||
* tags
|
||||
* photo info
|
||||
|
||||
*/
|
||||
|
||||
router.get("/", function(req, res/*, next*/) {
|
||||
let start = moment(req.query.start || null) || null,
|
||||
end = moment(req.query.end || null) || null;
|
||||
|
||||
let query = "SELECT DATE(taken) AS date,COUNT(*) AS count FROM photos WHERE path LIKE :path GROUP BY DATE(taken) ORDER BY date";
|
||||
return photoDB.sequelize.query(query, {
|
||||
replacements: {
|
||||
path: req.url.replace(/\?.*$/, "") + "%"
|
||||
},
|
||||
type: photoDB.Sequelize.QueryTypes.SELECT
|
||||
}).then(function(days) {
|
||||
|
||||
let results = {
|
||||
items: days
|
||||
};
|
||||
return res.status(200).json(results);
|
||||
}).catch(function(error) {
|
||||
console.error("Query failed: " + query);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
@ -30,8 +30,8 @@ router.get("/", function(req, res/*, next*/) {
|
||||
|
||||
if (req.query.next) {
|
||||
let parts = req.query.next.split("_");
|
||||
cursor = parts[1];
|
||||
id = parseInt(parts[0]);
|
||||
cursor = parts[0];
|
||||
id = parseInt(parts[1]);
|
||||
} else {
|
||||
cursor = "";
|
||||
id = -1;
|
||||
|
@ -139,8 +139,9 @@ const sharp = require("sharp"), exif = require("exif-reader");
|
||||
|
||||
function triggerWatcher() {
|
||||
setTimeout(triggerWatcher, 1000);
|
||||
|
||||
|
||||
if (!processRunning && processQueue.length) {
|
||||
let lastMessage = moment(), toProcess = processQueue.length;
|
||||
processRunning = true;
|
||||
return Promise.map(processQueue, function(entry) {
|
||||
var path = entry[0], file = entry[1], created = entry[2], albumId = entry[3],
|
||||
@ -172,20 +173,23 @@ function triggerWatcher() {
|
||||
if (metadata.exif && metadata.exif.exif && metadata.exif.exif.DateTimeOriginal && !isNaN(metadata.exif.exif.DateTimeOriginal.valueOf())) {
|
||||
metadata.exif.exif.DateTimeOriginal.setHours(0, 0, 0, 0);
|
||||
metadata.exif.exif.DateTimeOriginal = metadata.exif.exif.DateTimeOriginal.toISOString().replace(/T.*/, "");
|
||||
// console.log(metadata.exif.exif.DateTimeOriginal);
|
||||
replacements.taken = moment(metadata.exif.exif.DateTimeOriginal, "YYYY-MM-DD").format().replace(/T.*/, "");
|
||||
replacements.modified = moment(metadata.exif.exif.DateTimeOriginal).format().replace(/T.*/, "");
|
||||
|
||||
if (replacements.taken == "Invalid date") {
|
||||
console.log("Invalid EXIF date information: ", JSON.stringify(metadata.exif.exif));
|
||||
replacements.taken = replacements.modified = replacements.added;
|
||||
}
|
||||
} else {
|
||||
// console.log("Missing EXIF info for: " + file);
|
||||
//console.log(JSON.stringify(metadata.exif, null, 2));
|
||||
let patterns = /(20[0-9][0-9]-?[0-9][0-9]-?[0-9][0-9])[_\-]?([0-9]*)/, date = replacements.added;
|
||||
let match = file.match(patterns);
|
||||
if (match) {
|
||||
date = moment(match[1].replace(/-/g, ""), "YYYYMMDD").format();
|
||||
// console.log("Constructed date: " + date);
|
||||
if (date == "Invalid date") {
|
||||
date = moment(created).format();
|
||||
}
|
||||
} else {
|
||||
date = moment(created).format();
|
||||
// console.log("Date from file: ", src, date);
|
||||
}
|
||||
replacements.taken = replacements.modified = date;
|
||||
}
|
||||
@ -194,6 +198,12 @@ function triggerWatcher() {
|
||||
return photoDB.sequelize.query("INSERT INTO photos " +
|
||||
"SET albumId=:albumId,path=:path,filename=:filename,added=DATE(:added),modified=DATE(:modified),taken=DATE(:taken),width=:width,height=:height", {
|
||||
replacements: replacements
|
||||
}).then(function() {
|
||||
toProcess--;
|
||||
if (moment().add(-5, 'seconds') > lastMessage) {
|
||||
console.log("Items to be processed: " + toProcess);
|
||||
lastMessage = moment();
|
||||
}
|
||||
});
|
||||
}).catch(function(error) {
|
||||
console.log("Error resizing or writing " + src, error);
|
||||
|
Loading…
x
Reference in New Issue
Block a user