Added albums API

Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2018-08-27 15:17:39 -07:00
parent 29dafae886
commit bfd872f60a
6 changed files with 228 additions and 48 deletions

View File

@ -10,22 +10,22 @@
<dom-module id="photo-thumbnail"> <dom-module id="photo-thumbnail">
<template> <template>
<style is="custom-style" include="iron-flex iron-flex-alignment iron-positioning"> <style is="custom-style" include="iron-flex iron-flex-alignment iron-positioning">
:host { :host {
@apply --photo-thumbnail; @apply --photo-thumbnail;
display: inline-block; display: inline-block;
position: relative; position: relative;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
background-position: 50% 50%; background-position: 50% 50%;
border-radius: 3px; border-radius: 3px;
cursor: pointer; cursor: pointer;
color: white; color: white;
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
width: 200px; width: 200px;
height: 200px; height: 200px;
} }
:host > div { :host > div {
padding: 0.5em; padding: 0.5em;
@ -34,12 +34,17 @@
:host:hover { :host:hover {
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(0, 0, 0, 0.2);
} }
div[path]:hover {
text-decoration: underline;
cursor: pointer;
}
</style> </style>
<div class="layout vertical"> <div class="layout vertical">
<div>[[item.id]]</div> <div>[[item.id]]</div>
<div>[[date(item)]]</div> <div>[[date(item)]]</div>
<div>[[item.path]]</div> <div path on-tap="_pathTap">[[item.path]]</div>
<div>[[item.filename]]</div> <div>[[item.filename]]</div>
</div> </div>
</template> </template>
@ -61,6 +66,10 @@
} }
}, },
listeners: {
"tap": "_imageTap"
},
observers: [ observers: [
"widthChanged(width)", "widthChanged(width)",
"thumbChanged(thumbpath)" "thumbChanged(thumbpath)"
@ -76,7 +85,7 @@
}, },
safeItemThumbFilepath: function(item, base) { safeItemThumbFilepath: function(item, base) {
return "'" + (base + item.path + "/thumbs/" + item.filename).replace(/'/, "\\'") + "'"; return "'" + (base + encodeURI(item.path) + "/thumbs/" + encodeURI(item.filename)).replace(/'/, "\\'") + "'";
}, },
date: function(item) { date: function(item) {
@ -85,11 +94,15 @@
}, },
_imageTap: function(event) { _imageTap: function(event) {
window.open(this.base + event.model.item.path + "/" + event.model.item.filename, "image"); this.fire("load-image");
event.stopPropagation();
event.preventDefault();
}, },
_pathTap: function(event) { _pathTap: function(event) {
window.location.href = event.model.item.filepath; this.fire("load-album", this.item.path);
event.stopPropagation();
event.preventDefault();
}, },
attached: function() { attached: function() {

View File

@ -48,6 +48,15 @@
:host { :host {
} }
#breadcrumb > div {
margin-right: 0.5em;
cursor: pointer;
}
#breadcrumb > div:hover {
text-decoration: underline;
}
app-toolbar { app-toolbar {
background-color: rgba(64, 0, 64, 0.5); background-color: rgba(64, 0, 64, 0.5);
color: white; color: white;
@ -76,6 +85,10 @@
background-color: yellow; background-color: yellow;
} }
#header > * {
margin-right: 0.5em;
}
app-header-layout { app-header-layout {
--layout-fit: { --layout-fit: {
overflow-y: hidden !important; overflow-y: hidden !important;
@ -113,15 +126,17 @@
<app-header-layout reveals> <app-header-layout reveals>
<app-header fixed> <app-header fixed>
<div> <div id="header" class="layout horizontal center">
<paper-spinner active$="[[loading]]" class="thin"></paper-spinner> <paper-spinner active$="[[loading]]" class="thin"></paper-spinner>
<paper-radio-group selected="{{order}}"> <paper-radio-group selected="{{order}}">
<paper-radio-button name="by-date">By date</paper-radio-button> <paper-radio-button name="by-date">By date</paper-radio-button>
<paper-radio-button name="by-album">By album</paper-radio-button> <paper-radio-button name="by-album">By album</paper-radio-button>
</paper-radio-group> </paper-radio-group>
<paper-checkbox checked$="[[limitPerFolder]]" on-checked-changed="onLimitPerFolderChecked">Limit per folder</paper-checkbox> <paper-checkbox checked$="[[limitPerFolder]]" on-checked-changed="onLimitPerFolderChanged">Limit per folder</paper-checkbox>
<paper-checkbox checked$="[[breakOnDayChange]]" on-checked-changed="onBreakOnDayChanged">Break on day change</paper-checkbox> <paper-checkbox checked$="[[breakOnDayChange]]" on-checked-changed="onBreakOnDayChanged">Break on day change</paper-checkbox>
<div>[[path]]</div> <div id="breadcrumb" class="horizontal layout center"><template is="dom-repeat" items="[[breadcrumb(path)]]">
<div on-tap="loadPath">[[item.name]] /</div>
</template></div>
</div> </div>
</app-header> </app-header>
<div id="content"> <div id="content">
@ -174,11 +189,31 @@
} }
}, },
breadcrumb: function(path) {
var crumbs = path.split("/"), parts = [];
path = "";
crumbs.forEach(function(crumb, index) {
if (crumb) {
path += "/" + crumb;
}
parts.push({
name: crumb ? crumb : "Top",
path: path
})
});
return parts;
},
observers: [ observers: [
"widthChanged(calcWidth)" "widthChanged(calcWidth)",
"orderChanged(order)"
], ],
onLimitPerFolder: function(event) { orderChanged: function(order) {
},
onLimitPerFolderChanged: function(event) {
if (!this.photos) { if (!this.photos) {
return; return;
} }
@ -206,6 +241,25 @@
"iron-resize" : "onResize" "iron-resize" : "onResize"
}, },
loadPath: function(event) {
this.path = event.model.item.path;
Polymer.dom(this.$.thumbnails).innerHTML = "";
this.photos = [];
this.next = false;
this._loadPhotos();
},
loadAlbum: function(event) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
this.path = event.detail;
Polymer.dom(this.$.thumbnails).innerHTML = "";
this.photos = [];
this.next = false;
this._loadPhotos();
},
onScroll: function(event) { onScroll: function(event) {
if (this.disableScrolling) { if (this.disableScrolling) {
event.preventDefault(); event.preventDefault();
@ -300,35 +354,79 @@
}, },
loadNextPhotos: function() { loadNextPhotos: function() {
if (!this.photos.length) {
return;
}
var cursor = this.photos[this.photos.length - 1]; var cursor = this.photos[this.photos.length - 1];
this._loadPhotos(cursor.taken.toString().replace(/T.*/, "") + "_" + cursor.id, -1, true); this._loadPhotos(cursor.taken.toString().replace(/T.*/, "") + "_" + cursor.id, -1, true);
}, },
loadPrevPhotos: function() { loadPrevPhotos: function() {
if (!this.photos.length) {
return;
}
var cursor = this.photos[0]; var cursor = this.photos[0];
this._loadPhotos(cursor.taken.toString().replace(/T.*/, "") + "_" + cursor.id, +1); this._loadPhotos(cursor.taken.toString().replace(/T.*/, "") + "_" + cursor.id, +1);
}, },
appendItems: function(photos) { appendItems: function(photos) {
photos.forEach(function(photo) { var thisDay;
var thumbnail = document.createElement("photo-thumbnail"); if (this.limitPerFolder) {
console.log("Max per day: " + this.cols);
}
thisDay = 0;
for (var i = 0; i < photos.length; i++) {
var photo = photos[i],
thumbnail = document.createElement("photo-thumbnail"),
datetime;
thumbnail.item = photo; thumbnail.item = photo;
thumbnail.width = this.calcWidth; thumbnail.width = this.calcWidth;
thumbnail.addEventListener("click", this._imageTap.bind(this)); thumbnail.addEventListener("load-image", this._imageTap.bind(this));
thumbnail.addEventListener("load-album", this.loadAlbum.bind(this));
datetime = (photo.taken || photo.modified || photo.added).replace(/T.*$/, "");
if (this.breakOnDayChange) { if (this.breakOnDayChange) {
var datetime = (photo.taken || photo.modified || photo.added).replace(/T.*$/, ""), var dateBlock = this.querySelector("#date-" + datetime);
dateBlock = this.querySelector("#date-" + datetime);
if (!dateBlock) { if (!dateBlock) {
dateBlock = document.createElement("div"); dateBlock = document.createElement("div");
dateBlock.id = "date-" + datetime; dateBlock.id = "date-" + datetime;
dateBlock.classList.add("date-line"); dateBlock.classList.add("date-line");
dateBlock.textContent = datetime; dateBlock.textContent = datetime;
Polymer.dom(this.$.thumbnails).appendChild(dateBlock); Polymer.dom(this.$.thumbnails).appendChild(dateBlock);
thisDay = 0;
} else {
if (this.limitPerFolder) {
var thumbs = [], el = dateBlock.nextElementSibling;
while (el && el.tagName == "PHOTO-THUMBNAIL") {
thumbs.push(el);
el = el.nextElementSibling;
}
thisDay = thumbs.length;
while (thisDay > this.cols) {
Polymer.dom(thumbs[thisDay - 1].parentElement).removeChild(thumbs[thisDay - 1]);
thisDay--;
}
}
} }
} }
Polymer.dom(this.$.thumbnails).appendChild(thumbnail); if (!this.limitPerFolder || thisDay < this.cols) {
}.bind(this)); Polymer.dom(this.$.thumbnails).appendChild(thumbnail);
thisDay++;
}
if (this.limitPerFolder && thisDay == this.cols) {
while (i + 1 < photos.length) {
photo = photos[i + 1];
if (datetime != (photo.taken || photo.modified || photo.added).replace(/T.*$/, "")) {
break;
}
i++;
}
thisDay = 0;
}
}
}, },
_loadPhotos: function(start, dir, append) { _loadPhotos: function(start, dir, append) {
@ -341,7 +439,7 @@
var params = { var params = {
limit: Math.ceil(this.clientWidth / 200) * Math.ceil(this.clientHeight / 200), limit: Math.ceil(this.clientWidth / 200) * Math.ceil(this.clientHeight / 200),
dir: dir dir: dir
}, url = ""; }, query = "";
if (start) { if (start) {
params.next = start; params.next = start;
} }
@ -349,15 +447,15 @@
params.sort = this.sortOrder; params.sort = this.sortOrder;
} }
for (var key in params) { for (var key in params) {
if (url == "") { if (query == "") {
url = "?"; query = "?";
} else { } else {
url += "&"; query += "&";
} }
url += key + "=" + encodeURIComponent(params[key]); query += key + "=" + encodeURIComponent(params[key]);
} }
window.fetch("api/v1/photos" + url, function(error, xhr) { window.fetch("api/v1/photos" + (this.path || "") + query, function(error, xhr) {
this.loading = false; this.loading = false;
if (error) { if (error) {
console.error(JSON.stringify(error, null, 2)); console.error(JSON.stringify(error, null, 2));
@ -391,7 +489,7 @@
this.photos = results.items; this.photos = results.items;
} }
if (dir == +1) { if (dir == -1) {
this.prev = start ? true : false; this.prev = start ? true : false;
this.next = results.more ? true : false; this.next = results.more ? true : false;
} else { } else {

View File

@ -64,6 +64,7 @@ app.use(function(req, res, next){
app.use(basePath + "api/v1/photos", require("./routes/photos")); app.use(basePath + "api/v1/photos", require("./routes/photos"));
app.use(basePath + "api/v1/days", require("./routes/days")); app.use(basePath + "api/v1/days", require("./routes/days"));
app.use(basePath + "api/v1/albums", require("./routes/albums"));
/* Declare the "catch all" index route last; the final route is a 404 dynamic router */ /* Declare the "catch all" index route last; the final route is a 404 dynamic router */
app.use(basePath, require("./routes/index")); app.use(basePath, require("./routes/index"));

71
server/routes/albums.js Normal file
View File

@ -0,0 +1,71 @@
"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();
router.get("/*", function(req, res/*, next*/) {
let url = decodeURI(req.url).replace(/\?.*$/, ""),
query = "SELECT * FROM albums WHERE path=:path";
if (url == "/") {
url = "";
}
return photoDB.sequelize.query(query, {
replacements: {
path: url
},
type: photoDB.Sequelize.QueryTypes.SELECT
}).then(function(parent) {
if (parent.length == 0) {
return res.status(404).send(req.url + " not found");
}
parent = parent[0];
for (var key in parent) {
if (parent[key] instanceof Date) {
parent[key].setHours(0, 0, 0, 0);
parent[key] = moment(parent[key]);
}
}
return photoDB.sequelize.query("SELECT * FROM albums WHERE parentId=:parentId", {
replacements: {
parentId: parent.id
},
type: photoDB.Sequelize.QueryTypes.SELECT
}).then(function(children) {
children.forEach(function(album) {
for (var key in album) {
if (album[key] instanceof Date) {
album[key].setHours(0, 0, 0, 0);
album[key] = moment(album[key]);
}
}
});
let results = {
album: parent,
children: children
};
return res.status(200).json(results);
});
}).catch(function(error) {
console.error("Query failed: " + query);
return Promise.reject(error);
});
});
module.exports = router;

View File

@ -24,7 +24,7 @@ const router = express.Router();
*/ */
router.get("/", function(req, res/*, next*/) { router.get("/*", function(req, res/*, next*/) {
let limit = parseInt(req.query.limit) || 50, let limit = parseInt(req.query.limit) || 50,
order = (parseInt(req.query.dir) == -1) ? "DESC" : "", id, cursor, index; order = (parseInt(req.query.dir) == -1) ? "DESC" : "", id, cursor, index;
@ -59,7 +59,7 @@ router.get("/", function(req, res/*, next*/) {
return photoDB.sequelize.query(query, { return photoDB.sequelize.query(query, {
replacements: { replacements: {
cursor: cursor, cursor: cursor,
path: req.url.replace(/\?.*$/, "") + "%" path: decodeURI(req.url).replace(/\?.*$/, "") + "%"
}, },
type: photoDB.Sequelize.QueryTypes.SELECT type: photoDB.Sequelize.QueryTypes.SELECT
}).then(function(photos) { }).then(function(photos) {
@ -100,8 +100,8 @@ router.get("/", function(req, res/*, next*/) {
photos.slice(limit, photos.length); photos.slice(limit, photos.length);
} }
photos.forEach(function(photo) { photos.forEach(function(photo) {
photo.path = encodeURI(photo.path); // photo.path = encodeURI(photo.path);
photo.filename = encodeURI(photo.filename); // photo.filename = encodeURI(photo.filename);
}); });
let results = { let results = {

View File

@ -17,7 +17,7 @@ function scanDir(parent, path) {
let extensions = [ "jpg", "jpeg", "png", "gif", "nef" ], let extensions = [ "jpg", "jpeg", "png", "gif", "nef" ],
re = new RegExp("\.((" + extensions.join(")|(") + "))$", "i"), re = new RegExp("\.((" + extensions.join(")|(") + "))$", "i"),
replacements = { replacements = {
path: path, path: path.slice(picturesPath.length),
parent: parent || null parent: parent || null
}; };
@ -35,10 +35,7 @@ function scanDir(parent, path) {
if (results.length == 0) { if (results.length == 0) {
// console.log("Adding " + path + " under " + parent, replacements); // console.log("Adding " + path + " under " + parent, replacements);
return photoDB.sequelize.query("INSERT INTO albums (path,parentId) VALUES(:path,:parent)", { return photoDB.sequelize.query("INSERT INTO albums (path,parentId) VALUES(:path,:parent)", {
replacements: { replacements: replacements
path: path,
parent: parent || null
},
}).then(function(results) { }).then(function(results) {
return results[1].lastID; return results[1].lastID;
}); });
@ -47,7 +44,7 @@ function scanDir(parent, path) {
} }
}).then(function(parent) { }).then(function(parent) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
console.log("Scanning path " + path + " under parent " + parent); console.log("Scanning " + replacements.path);
fs.readdir(path, function(err, files) { fs.readdir(path, function(err, files) {
if (err) { if (err) {