From bfd872f60ad7da059caa3901da86ed3f2edd561a Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Mon, 27 Aug 2018 15:17:39 -0700 Subject: [PATCH] Added albums API Signed-off-by: James Ketrenos --- frontend/elements/photo-thumbnail.html | 51 +++++--- frontend/src/ketr-photos/ketr-photos.html | 136 +++++++++++++++++++--- server/app.js | 1 + server/routes/albums.js | 71 +++++++++++ server/routes/photos.js | 8 +- server/scanner.js | 9 +- 6 files changed, 228 insertions(+), 48 deletions(-) create mode 100644 server/routes/albums.js diff --git a/frontend/elements/photo-thumbnail.html b/frontend/elements/photo-thumbnail.html index 612f15b..e722c73 100644 --- a/frontend/elements/photo-thumbnail.html +++ b/frontend/elements/photo-thumbnail.html @@ -10,22 +10,22 @@ @@ -61,6 +66,10 @@ } }, + listeners: { + "tap": "_imageTap" + }, + observers: [ "widthChanged(width)", "thumbChanged(thumbpath)" @@ -76,7 +85,7 @@ }, 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) { @@ -85,11 +94,15 @@ }, _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) { - window.location.href = event.model.item.filepath; + this.fire("load-album", this.item.path); + event.stopPropagation(); + event.preventDefault(); }, attached: function() { diff --git a/frontend/src/ketr-photos/ketr-photos.html b/frontend/src/ketr-photos/ketr-photos.html index c3107ca..785bc26 100755 --- a/frontend/src/ketr-photos/ketr-photos.html +++ b/frontend/src/ketr-photos/ketr-photos.html @@ -48,6 +48,15 @@ :host { } + #breadcrumb > div { + margin-right: 0.5em; + cursor: pointer; + } + + #breadcrumb > div:hover { + text-decoration: underline; + } + app-toolbar { background-color: rgba(64, 0, 64, 0.5); color: white; @@ -76,6 +85,10 @@ background-color: yellow; } + #header > * { + margin-right: 0.5em; + } + app-header-layout { --layout-fit: { overflow-y: hidden !important; @@ -113,15 +126,17 @@ -
+
@@ -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: [ - "widthChanged(calcWidth)" + "widthChanged(calcWidth)", + "orderChanged(order)" ], - onLimitPerFolder: function(event) { + orderChanged: function(order) { + + }, + + onLimitPerFolderChanged: function(event) { if (!this.photos) { return; } @@ -206,6 +241,25 @@ "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) { if (this.disableScrolling) { event.preventDefault(); @@ -300,35 +354,79 @@ }, loadNextPhotos: function() { + if (!this.photos.length) { + return; + } var cursor = this.photos[this.photos.length - 1]; this._loadPhotos(cursor.taken.toString().replace(/T.*/, "") + "_" + cursor.id, -1, true); }, loadPrevPhotos: function() { + if (!this.photos.length) { + return; + } var cursor = this.photos[0]; this._loadPhotos(cursor.taken.toString().replace(/T.*/, "") + "_" + cursor.id, +1); }, appendItems: function(photos) { - photos.forEach(function(photo) { - var thumbnail = document.createElement("photo-thumbnail"); + var thisDay; + 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.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) { - var datetime = (photo.taken || photo.modified || photo.added).replace(/T.*$/, ""), - dateBlock = this.querySelector("#date-" + datetime); + var 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); + 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); - }.bind(this)); + if (!this.limitPerFolder || thisDay < this.cols) { + 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) { @@ -341,7 +439,7 @@ var params = { limit: Math.ceil(this.clientWidth / 200) * Math.ceil(this.clientHeight / 200), dir: dir - }, url = ""; + }, query = ""; if (start) { params.next = start; } @@ -349,15 +447,15 @@ params.sort = this.sortOrder; } for (var key in params) { - if (url == "") { - url = "?"; + if (query == "") { + query = "?"; } 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; if (error) { console.error(JSON.stringify(error, null, 2)); @@ -391,7 +489,7 @@ this.photos = results.items; } - if (dir == +1) { + if (dir == -1) { this.prev = start ? true : false; this.next = results.more ? true : false; } else { diff --git a/server/app.js b/server/app.js index 52bce55..8528e01 100644 --- a/server/app.js +++ b/server/app.js @@ -64,6 +64,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")); +app.use(basePath + "api/v1/albums", require("./routes/albums")); /* Declare the "catch all" index route last; the final route is a 404 dynamic router */ app.use(basePath, require("./routes/index")); diff --git a/server/routes/albums.js b/server/routes/albums.js new file mode 100644 index 0000000..14d6d78 --- /dev/null +++ b/server/routes/albums.js @@ -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; diff --git a/server/routes/photos.js b/server/routes/photos.js index 93aa6c5..d19aa45 100644 --- a/server/routes/photos.js +++ b/server/routes/photos.js @@ -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, 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, { replacements: { cursor: cursor, - path: req.url.replace(/\?.*$/, "") + "%" + path: decodeURI(req.url).replace(/\?.*$/, "") + "%" }, type: photoDB.Sequelize.QueryTypes.SELECT }).then(function(photos) { @@ -100,8 +100,8 @@ router.get("/", function(req, res/*, next*/) { photos.slice(limit, photos.length); } photos.forEach(function(photo) { - photo.path = encodeURI(photo.path); - photo.filename = encodeURI(photo.filename); +// photo.path = encodeURI(photo.path); +// photo.filename = encodeURI(photo.filename); }); let results = { diff --git a/server/scanner.js b/server/scanner.js index 2a2ce48..175fcd5 100644 --- a/server/scanner.js +++ b/server/scanner.js @@ -17,7 +17,7 @@ function scanDir(parent, path) { let extensions = [ "jpg", "jpeg", "png", "gif", "nef" ], re = new RegExp("\.((" + extensions.join(")|(") + "))$", "i"), replacements = { - path: path, + path: path.slice(picturesPath.length), parent: parent || null }; @@ -35,10 +35,7 @@ function scanDir(parent, path) { if (results.length == 0) { // console.log("Adding " + path + " under " + parent, replacements); return photoDB.sequelize.query("INSERT INTO albums (path,parentId) VALUES(:path,:parent)", { - replacements: { - path: path, - parent: parent || null - }, + replacements: replacements }).then(function(results) { return results[1].lastID; }); @@ -47,7 +44,7 @@ function scanDir(parent, path) { } }).then(function(parent) { 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) { if (err) {