ketr.photos/frontend/src/ketr-photos/ketr-photos.html
James Ketrenos c05f1ab8e5 Keep same album on subsequent data load call to /photos
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
2018-08-30 09:04:19 -07:00

678 lines
21 KiB
HTML
Executable File

<!doctype html>
<html>
<head>
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../bower_components/app-layout/app-header-layout/app-header-layout.html">
<link rel="import" href="../../bower_components/app-layout/app-drawer-layout/app-drawer-layout.html">
<link rel="import" href="../../bower_components/app-layout/app-header/app-header.html">
<link rel="import" href="../../bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel="import" href="../../bower_components/app-layout/app-drawer/app-drawer.html">
<link rel="import" href="../../bower_components/app-route/app-location.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">
<link rel="import" href="../../bower_components/iron-resizable-behavior/iron-resizable-behavior.html">
<link rel="import" href="../../bower_components/paper-button/paper-button.html">
<link rel="import" href="../../bower_components/paper-checkbox/paper-checkbox.html">
<link rel="import" href="../../bower_components/paper-dialog/paper-dialog.html">
<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-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-spinner/paper-spinner.html">
<link rel="import" href="../../bower_components/paper-toast/paper-toast.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>
<style>
body,* {
font-family: Helvetica Neue,Helvetica,Arial,sans-serif;
}
b,strong {
font-family: Helvetica Neue,Helvetica,Arial,sans-serif;
}
</style>
</head>
<dom-module id="ketr-photos">
<template>
<style is="custom-style" include="iron-flex iron-flex-alignment iron-positioning">
:host {
}
#breadcrumb {
padding: 0.5em;
}
#albums {
height: 100%;
overflow-y: scroll;
}
#albums > div {
margin: 0.5em;
cursor: pointer;
}
#breadcrumb > div {
margin-right: 0.5em;
cursor: pointer;
}
#albums div:hover,
#breadcrumb > div:hover {
text-decoration: underline;
}
app-toolbar {
background-color: rgba(64, 0, 64, 0.5);
color: white;
}
#toast[error] {
--paper-toast-background-color: red;
--paper-toast-color: white;
}
#placeholder {
position: absolute;
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%;
}
.album-line {
display: block;
padding: 0.5em 0;
width: 100%;
cursor: pointer;
}
.album-line > div {
margin-right: 0.5em;
}
.album-line > div:hover {
text-decoration: underline;
}
photo-thumbnail {
--photo-thumbnail: {
border: 3px solid rgba(0, 0, 0, 0);
};
}
photo-thumbnail[selected] {
--photo-thumbnail: {
border-color: blue;
};
}
</style>
<app-location route="{{route}}"></app-location>
<app-drawer-layout id="albumLayout">
<app-drawer persistent id="albumList" slot="drawer">
<div id="albums" class="layout vertical">
<template is="dom-repeat" items="[[breadcrumb(path)]]">
<div on-tap="loadPath">[[item.name]] /</div>
</template>
<template is="dom-repeat" items="[[albums.children]]">
<div on-tap="loadPath">[[item.name]]</div>
</template>
</div>
</app-drawer>
<app-header-layout>
<app-header reveals slot="header">
<div class="layout vertical start">
<div class="layout horizontal center">
<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>
<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>
</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>
</app-header>
<div id="content" fullbleed class="flex layout vertical">
<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>
<paper-button disabled$="[[!next]]" on-tap="loadNextPhotos">next</paper-button>
</div>
</div>
</app-header-layout>
</app-drawer-layout>
<paper-toast id="toast"></paper-toast>
<photo-lightbox tabindex="0" id="lightbox" on-close="lightBoxClose" on-next="lightBoxNext" on-previous="lightBoxPrevious"></photo-lightbox>
<photo-thumbnail id="placeholder"></photo-thumbnail>
</template>
<script>
document.addEventListener("WebComponentsReady", function() {
"use strict";
Polymer({
is: "ketr-photos",
properties: {
order: {
type: String,
value: "by-date"
},
"loading": Boolean,
"photos": Array,
prev: {
type: Boolean,
value: false
},
next: {
type: Boolean,
value: false
},
breakOnDayChange: {
type: Boolean,
value: true,
reflectToAttribute: true
},
limitPerFolder: {
type: Boolean,
value: false,
reflectToAttribute: true
},
showAlbums: {
type: Boolean,
computed: "shouldShowAlbums(order)"
},
path: {
type: String,
value: ""
}
},
shouldShowAlbums: function(order) {
return order == "by-album";
},
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)",
"orderChanged(order)"
],
orderChanged: function(order) {
console.log("Order: " + order);
if (order == "by-album") {
this.$.albumLayout.forceNarrow = false;
this.$.albumList.open();
this.$.albumList.persistent = true;
} else if (order == "by-date") {
this.$.albumList.close();
this.$.albumLayout.forceNarrow = true;
this.$.albumList.resetLayout();
}
Polymer.dom(this.$.thumbnails).innerHTML = "";
this.appendItems(this.photos);
},
onLimitPerFolderChanged: function(event) {
if (!this.photos) {
return;
}
this.limitPerFolder = event.detail.value;
Polymer.dom(this.$.thumbnails).innerHTML = "";
this.appendItems(this.photos);
},
onBreakOnDayChanged: function(event) {
if (!this.photos) {
return;
}
this.breakOnDayChange = event.detail.value;
Polymer.dom(this.$.thumbnails).innerHTML = "";
this.appendItems(this.photos);
},
listeners: {
"scroll": "onScroll",
"iron-resize" : "onResize"
},
loadPath: function(event) {
this.path = event.model.item.path;
Polymer.dom(this.$.thumbnails).innerHTML = "";
this.photos = [];
this.next = false;
this._loadAlbums();
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._loadAlbums();
this._loadPhotos();
},
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) {
thumb.width = calcWidth;
});
},
behaviors: [
/* @polymerBehavior Polymer.IronResizableBehavior */
Polymer.IronResizableBehavior
],
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) {
this.loadLightbox(event.currentTarget);
},
_pathTap: function(event) {
window.location.href = event.model.item.filepath;
},
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) {
if (!photos) {
return;
}
var thisDay, lastPath = null;
if (this.limitPerFolder) {
console.log("Max per day: " + this.cols);
}
var albums = this.querySelectorAll(".album-line");
if (albums.length) {
lastPath = albums[albums.length - 1];
}
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("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.order == "by-album") {
if (lastPath != photo.path) {
lastPath = photo.path;
var albumBlock = document.createElement("div");
albumBlock.classList.add("album-line");
albumBlock.classList.add("layout");
albumBlock.classList.add("horizontal");
var trail = this.breadcrumb(lastPath);
trail.forEach(function(crumb) {
var div = document.createElement("div");
div.path = crumb.path;
div.textContent = crumb.name + " /";
div.addEventListener("tap", this.pathTapped.bind(this));
albumBlock.appendChild(div);
}.bind(this));
Polymer.dom(this.$.thumbnails).appendChild(albumBlock);
}
}
if (this.breakOnDayChange) {
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--;
}
}
}
}
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;
}
}
},
pathTapped: function(event) {
this.path = event.currentTarget.path;
Polymer.dom(this.$.thumbnails).innerHTML = "";
this.photos = [];
this.next = false;
this._loadAlbums();
this._loadPhotos();
},
_loadPhotos: function(start, dir, append) {
if (this.loading == true) {
return;
}
this.loading = true;
dir = dir || -1;
var params = {
limit: Math.ceil(this.clientWidth / 200) * Math.ceil(this.clientHeight / 200),
dir: dir
}, query = "";
if (start) {
params.next = start;
}
if (this.sortOrder) {
params.sort = this.sortOrder;
}
for (var key in params) {
if (query == "") {
query = "?";
} else {
query += "&";
}
query += key + "=" + encodeURIComponent(params[key]);
}
window.fetch("api/v1/photos" + (this.path || "") + query, function(error, xhr) {
this.loading = false;
if (error) {
console.error(JSON.stringify(error, null, 2));
return;
}
var results;
try {
results = JSON.parse(xhr.responseText);
} catch (___) {
this.$.toast.text = "Unable to load/parse photo list.";
this.$.toast.setAttribute("error", true);
this.$.toast.updateStyles();
this.$.toast.show();
console.error("Unable to parse photos");
return;
}
var base = document.querySelector("base");
if (base) {
this.base = new URL(base.href).pathname.replace(/\/$/, ""); /* Remove trailing slash if there */
} else {
this.base = "";
}
this.appendItems(results.items);
if (append) {
this.photos = this.photos.concat(results.items);
} else {
this.photos = results.items;
}
if (dir == -1) {
this.prev = start ? true : false;
this.next = results.more ? true : false;
} else {
this.prev = results.more ? true : false;
this.next = true;
}
}.bind(this));
},
_loadAlbums: function() {
if (this.loadingAlbums == true) {
return;
}
this.loadingAlbums = true;
window.fetch("api/v1/albums" + (this.path || ""), function(error, xhr) {
this.loadingAlbums = false;
if (error) {
console.error(JSON.stringify(error, null, 2));
return;
}
var results;
try {
results = JSON.parse(xhr.responseText);
} catch (___) {
this.$.toast.text = "Unable to load/parse album list.";
this.$.toast.setAttribute("error", true);
this.$.toast.updateStyles();
this.$.toast.show();
console.error("Unable to parse photos");
return;
}
this.albums = results;
}.bind(this));
},
onResize: function(event) {
this.debounce("resize", function() {
var width = Math.max(this.$.placeholder.offsetWidth || 0, 200);
this.cols = Math.floor(this.$.thumbnails.clientWidth / width);
var calc = width + Math.floor((this.$.thumbnails.clientWidth % width) / this.cols);
if (calc != this.calcWidth) {
this.calcWidth = calc;
}
}, 100);
},
ready: function() {
window.addEventListener("hashchange", function(event) {
this.hash = event.newURL.replace(/^[^#]*/, "");
}.bind(this), false);
/* Hash changes due to anchor clicks aren't firing the 'hashchange'
* event... possibly due to app-location? */
window.setInterval(function() {
if (this.hash != window.location.hash) {
this.hash = window.location.hash;
}
}.bind(this), 100);
window.setInterval(function() {
function isElementInViewport(el) {
var rect = el.getBoundingClientRect(),
vWidth = window.innerWidth || doc.documentElement.clientWidth,
vHeight = window.innerHeight || doc.documentElement.clientHeight;
// Return false if it's not in the viewport
if (rect.right < 0 || rect.bottom < 0
|| rect.left > vWidth || rect.top > vHeight) {
return false;
}
return true;
}
if (this.next && isElementInViewport(this.$.magic)) {
this.loadNextPhotos();
}
}.bind(this), 500);
this._loadAlbums();
this._loadPhotos();
this.onResize();
document.addEventListener("scroll", this.onScroll.bind(this));
}
});
});
</script>
</dom-module>
</html>