ketr.photos/frontend/src/ketr-photos/ketr-photos.html
James Ketrenos 1566d132f6 Cleaned up login/account creation page
Signed-off-by: James Ketrenos <james_git@ketrenos.com>
2018-09-28 01:48:02 -07:00

1301 lines
42 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-drawer/app-drawer.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-iconset/iron-iconset.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/iron-flex-layout/iron-flex-layout-classes.html">
<link rel="import" href="../../bower_components/paper-button/paper-button.html">
<link rel="import" href="../../bower_components/paper-dialog/paper-dialog.html">
<link rel="import" href="../../bower_components/paper-input/paper-input.html">
<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../../bower_components/paper-spinner/paper-spinner.html">
<link rel="import" href="../../bower_components/paper-tabs/paper-tab.html">
<link rel="import" href="../../bower_components/paper-tabs/paper-tabs.html">
<link rel="import" href="../../bower_components/paper-toast/paper-toast.html">
<link rel="import" href="../../bower_components/datetime-picker/calendar-element.html">
<script src="../../bower_components/moment/moment.js"></script>
<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-flex-factors iron-positioning">
:host {
}
#login {
max-width: 50ex;
margin: 1em auto;
padding: 2em;
border: 1px solid #444;
box-sizing: border-box;
}
#header {
padding: 0.5em;
background: #ddd;
box-shadow: 0px 0px 5px black;
box-sizing: border-box;
height: 64px;
}
#header iron-pages {
padding: 0 0.5em;
}
#header [mode='memories'] b:hover {
cursor: pointer;
text-decoration: underline;
}
#breadcrumb {
padding: 0.5em;
}
#bottom {
margin-top: 3em;
padding-bottom: 1em;
}
#albums {
overflow-y: scroll;
}
#pages > div {
box-sizing: border-box;
position: relative;
height: 100%;
}
#pages > div > div {
padding: 0.5em;
cursor: pointer;
}
#drawer {
}
#pages {
position: relative;
height: calc(100vh - 64px);
box-sizing: border-box;
}
#tabs {
background: #ddd;
color: #444;
box-sizing: border-box;
height: 64px;
box-shadow: 0px 0px 5px black;
}
#breadcrumb > div {
margin-right: 0.5em;
cursor: pointer;
}
#albums div:hover,
#breadcrumb > div:hover {
text-decoration: underline;
}
#yearSlider {
margin-top: 0.5em;
position: fixed;
display: block;
opacity: 0;
right: 5px;
bottom: 0px;
padding: 0.5em;
width: 120px;
background-color: rgb(16, 0, 16);
color: white;
text-align: center;
transition: opacity 0.5s ease-in-out;
-webkit-transition: opacity 0.5s ease-in-out;
}
#pager {
margin-top: 0.5em;
position: fixed;
display: block;
opacity: 0;
right: 5px;
border-radius: 0.25em;
padding: 0.5em;
width: 120px;
background-color: rgb(16, 0, 16);
color: white;
text-align: center;
transition: opacity 0.5s ease-in-out;
-webkit-transition: opacity 0.5s ease-in-out;
}
#toast[error] {
--paper-toast-background-color: red;
--paper-toast-color: white;
}
#thumbnails {
/* justify-content: space-between;*/
}
.date-line {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0.5em 0;
font-weight: bold;
}
.date-line .album-line {
cursor: pointer;
color: #ddd;
}
.album-line > div:not(:last-child) {
margin-right: 0.5em;
}
.album-line > div:first-child {
margin-left: 0.5em;
}
.album-line > div:hover {
text-decoration: underline;
}
.date-line {
/* margin-left: 2em;*/
}
.date-line .header {
padding: 0.25em 3px;
}
@media (max-width: 800px) {
.date-line {
font-size: 8pt;
}
photo-thumbnail {
width: 25vw;
height: 25vw;
}
}
photo-thumbnail {
--photo-thumbnail: {
border: 3px solid white;
};
}
photo-thumbnail[selected] {
--photo-thumbnail: {
border: 3px solid blue;
};
}
div:focus {
font-weight: bold;
outline: none;
}
calendar-element {
font-size: 0.8em;
}
#memories {
padding: 0.5em 1em;
}
#memories > div,
#memories > calendar-element {
padding-top: 1em;
}
#memories .memory-buttons {
padding-top: 0;
font-size: 0.8em;
}
#memories .memory-buttons :not([is-today]) paper-icon-button {
color: #444;
}
#memories [is-today] {
font-weight: bold;
}
#requestAccess {
max-width: 60ex;
border: 1px solid #444;
box-sizing: border-box;
}
#requestAccess div > div {
padding: 0.5em;
}
#requestAccess .title {
background-color: #ddd;
}
</style>
<app-location route="{{route}}"></app-location>
<app-drawer-layout id="albumLayout" force-narrow=true>
<app-drawer id="drawer" slot="drawer">
<paper-tabs attr-for-selected="tab" id="tabs" selected="{{mode}}">
<!--paper-tab tab="time"><paper-icon-button icon="date-range"></paper-icon-button></paper-tab-->
<paper-tab tab="memories"><paper-icon-button icon="today"></paper-icon-button></paper-tab>
<paper-tab tab="albums"><paper-icon-button icon="folder"></paper-icon-button></paper-tab>
</paper-tabs>
<iron-pages id="pages" attr-for-selected="id" selected="[[mode]]" fallback-selection="memories">
<div id="time"><div>... time slider ...</div></div>
<div id="memories" class="flex layout vertical center">
<div class="memory-buttons layout self-stretch horizontal around-justified">
<div is-today$=[[!isToday(date)]] on-tap="gotoRandomDay" class="layout vertical center">
<div>Random</div>
<paper-icon-button icon="refresh"></paper-icon-button>
</div>
<div is-today$=[[isToday(date)]] on-tap="gotoToday" class="layout vertical center">
<div>Today</div>
<paper-icon-button icon="today"></paper-icon-button>
</div>
</div>
<calendar-element
id="calendar"
tabindex
min="2016-01-01"
max="2016-12-31"
date="{{date}}"></calendar-element>
<div>Pick a date on the calendar to look back in time and see photos from that day.</div>
<div hidden$="[[!thumbnails.length]]">
<div>On <b>[[memoryDate]]</b>, there have been <b>[[add(thumbnails.length,pendingPhotos.length)]]</b> photos taken over <b>[[years.length]]</b> year(s).</div>
</div>
</div>
<div id="albums" class="flex layout vertical">
<template is="dom-repeat" items="[[breadcrumb(path)]]">
<div tabindex="0" on-tap="loadPath">[[item.name]] /</div>
</template>
<template is="dom-repeat" items="[[albums.children]]">
<div tabindex="0" on-tap="loadPath">[[item.name]]</div>
</template>
</div>
</iron-pages>
</app-drawer>
<app-header-layout>
<app-header reveals slot="header">
<div id="header" class="layout horizontal center">
<paper-icon-button icon="search" on-tap="drawerToggle"></paper-icon-button>
<iron-pages class="flex" attr-for-selected="mode" selected="[[mode]]">
<div mode="login"><div>You are not logged in.</div></div>
<div mode="albums" id="breadcrumb" class="horizontal layout center">
<template is="dom-repeat" items="[[breadcrumb(path)]]">
<div tabindex="0" on-tap="loadPath">[[item.name]] /</div>
</template>
</div>
<div mode="time">time</div>
<div mode="memories">Photos taken on <b on-tap="drawerToggle">[[memoryDate]]</b></div>
</iron-pages>
<div>
<div hidden$="[[user]]">
<!--paper-button>login</paper-button-->
</div>
<div hidden$="[[!user]]">
<paper-button on-tap="logout">logout</paper-button>
</div>
</div>
</div>
</app-header>
<div>
<div hidden$="[[!user]]">
<div id="thumbnails" class="layout horizontal wrap"></div>
<div id="bottom" class="layout vertical center">
<paper-spinner hidden$="[[!loading]]" active$="[[loading]]" class="thin"></paper-spinner>
<div hidden$="[[loading]]">~ the end ~</div>
</div>
</div>
<div id="login" hidden$="[[user]]" class="layout horizontal center">
<div class="flex layout vertical">
<div id="instructions">
<p><b>ketrenos.com</b> is a personal website for my family and friends.</p>
<p>If you already have an email account on this domain, you can login to the photo viewer
using your normal @ketrenos.com account name, and password.</p>
</p>If you are a friend or family member (immediate, or extended)
<a on-tap="requestAccess" href="#">request access</a>,
provide your email address, and tell me who in the extended Ketrenos
universe you know. If you're not a bot, I'll very likely give you access :)</p>
</div>
<paper-input tabindex=0 autofocus id="username" label="User ID" value="{{username}}" on-keypress="enterCheck"></paper-input>
<paper-input tabindex=0 id="password" label="Password" type="password" value="{{password}}" on-keypress="enterCheck"></paper-input>
<paper-button tabindex=0 id="loginButton" disabled$="[[disableLogin(username,password)]]" on-tap="login" raised><div hidden$="[[loggingIn]]">login</div><div hidden$="[[!loggingIn]]"><paper-spinner active$="[[loggingIn]]"></paper-spinner></div></paper-button>
</div>
</div>
</div>
</app-header-layout>
</app-drawer-layout>
<div id="pager">pager</div>
<div id="yearSlider">
<template is="dom-repeat" items="[[years]]">
<div>item</div>
</template>
</div>
<paper-dialog id="requestAccess" modal>
<div class="layout vertical">
<div class="title">Create an account</div>
<div>
<p>To have your account activated, tell me who you know in the 'who do you know?' field.</p>
<p>Thanks,</p>
<p>James</p>
</div>
<paper-input tabindex=0 autofocus id="mail" label="E-mail" value="{{mail}}" on-keypress="enterCheck"></paper-input>
<paper-input tabindex=0 id="password" label="Password" type="password" value="{{password}}" on-keypress="enterCheck"></paper-input>
<paper-input tabindex=0 id="name" label="Display name" value="{{name}}" on-keypress="enterCheck"></paper-input>
<paper-input tabindex=0 id="who" label="Who do you know?" value="{{who}}" on-keypress="enterCheck"></paper-input>
<paper-button tabindex=0 id="createButton" disabled$="[[disableCreate(mail,password,name,who)]]" on-tap="create" raised><div hidden$="[[loggingIn]]">create</div><div hidden$="[[!loggingIn]]"><paper-spinner active$="[[loggingIn]]"></paper-spinner></div></paper-button>
</div>
</paper-dialog>
<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>
document.addEventListener("WebComponentsReady", function() {
"use strict";
Polymer({
is: "ketr-photos",
properties: {
password: {
type: String,
value: ""
},
username: {
type: String,
value: ""
},
name: {
type: String,
value: ""
},
mail: {
type: String,
value: ""
},
years: {
type: Array,
value: []
},
user: {
type: Object,
value: null
},
order: {
type: String,
value: "by-date"
},
loading: Boolean,
pendingPhotos: {
type: Array,
value: []
},
next: {
type: Boolean,
value: false
},
breakOnDayChange: {
type: Boolean,
value: true,
reflectToAttribute: true
},
showAlbums: {
type: Boolean,
computed: "shouldShowAlbums(order)"
},
path: {
type: String,
value: ""
},
visibleThumbs: {
type: Array,
value: []
},
thumbnails: {
type: Array,
value: []
},
mode: {
type: String,
value: "login"
},
date: {
type: String,
value: "2016-" + window.moment().format("MM-DD")
}
},
observers: [
"widthChanged(calcWidth)",
"modeChanged(mode)",
"dateChanged(date)",
"userChanged(user)"
],
disableLogin: function(username, password) {
return !username || username == "" || !password || password == "";
},
disableCreate: function(mail, password, name, who) {
return !mail || mail == "" ||
!password || password == "" ||
!name || name == "" ||
!who || who == "";
},
enterCheck: function(event) {
if (event.key == 'Enter') {
var next = event.currentTarget.nextElementSibling;
event.preventDefault();
if (next.tagName.toLowerCase() == "paper-button") {
if (!next.disabled) {
next.click();
}
} else {
this.async(function(next) {
next._focusableElement.focus();
}.bind(this, next), 100);
return;
}
}
},
requestAccess: function(event) {
this.$.requestAccess.open();
},
add: function(a, b) {
return parseInt(a) + parseInt(b);
},
isToday: function(date) {
return this.date == "2016-" + window.moment().format("MM-DD");
},
gotoToday: function() {
this.date = "2016-" + window.moment().format("MM-DD");
},
gotoRandomDay: function() {
this.date = "2016-" + window.moment(Math.ceil(Math.random() * 365), "DDD").format("MM-DD");
},
shouldShowAlbums: function(order) {
return order == "by-album";
},
login: function(event) {
if (this.loading) {
return;
}
this.loading = true;
this.loggingIn = true;
this.user = null;
window.fetch("api/v1/users/login", function(error, xhr) {
this.loggingIn = false;
this.loading = false;
this.password = "";
var user;
if (error) {
this.user = null;
this.$.toast.text = error;
this.$.toast.setAttribute("error", true);
this.$.toast.updateStyles();
this.$.toast.show();
console.error("Invalid login information.");
return;
}
try {
user = JSON.parse(xhr.responseText);
} catch(___) {
this.$.toast.text = "Unable to load/parse user information.";
this.$.toast.setAttribute("error", true);
this.$.toast.updateStyles();
this.$.toast.show();
console.error("Unable to parse user information");
return;
}
if (user && user.username) {
this.user = user;
}
}.bind(this), null, "POST", { u: this.username, p: this.password });
},
create: function(event) {
if (this.loading) {
return;
}
this.loading = true;
this.loggingIn = true;
this.user = null;
window.fetch("api/v1/users/create", function(error, xhr) {
this.loggingIn = false;
this.loading = false;
this.password = "";
var user;
this.$.requestAccess.close();
if (error) {
this.user = null;
this.$.toast.text = error;
this.$.toast.setAttribute("error", true);
this.$.toast.updateStyles();
this.$.toast.show();
console.error("Invalid login information.");
return;
}
try {
user = JSON.parse(xhr.responseText);
} catch(___) {
this.$.toast.text = "Unable to load/parse user information.";
this.$.toast.setAttribute("error", true);
this.$.toast.updateStyles();
this.$.toast.show();
console.error("Unable to parse user information");
return;
}
if (user && user.username) {
this.user = user;
}
}.bind(this), null, "POST", {
w: this.who,
p: this.password,
n: this.name,
m: this.mail
});
},
logout: function(event) {
window.fetch("api/v1/users/logout", function(error, xhr) {
this.user = null;
}.bind(this));
},
changeMode: function(event) {
var mode = event.currentTarget.icon;
if (this.mode != mode) {
this.mode = mode;
}
},
resetPhotos: function() {
this.albums = [];
this.years = [];
this.notifyPath("years.length");
this.pendingPhotos = [];
this.notifyPath("pendingPhotos.length");
this.visibleThumbs = [];
this.thumbnails = [];
this.notifyPath("thumbnails.length");
this.cursor = null;
Polymer.dom(this.$.thumbnails).innerHTML = "";
this.next = false;
this.limit = undefined;
},
dateChanged: function(date) {
this.resetPhotos();
this.memoryDate = window.moment(this.date).format("MMMM Do");
this._loadPhotos();
},
modeChanged: function(mode) {
if (!mode) {
return;
}
console.log("Mode changed to " + mode);
this.path = "";
this.resetPhotos();
if (mode != "login") {
this._loadAlbums();
this._loadPhotos();
}
},
breadcrumb: function(path) {
var crumbs = path.replace(/(.*)\/$/, "/$1").split("/"), parts = [];
path = "";
crumbs.forEach(function(crumb, index) {
if (index > 0) {
path += crumb + "/";
}
parts.push({
name: crumb ? crumb : "Top",
path: path
})
});
return parts;
},
drawerToggle: function(event) {
if (this.$.drawer.opened) {
this.$.drawer.close();
this.$.albumLayout.forceNarrow = true;
this.$.drawer.resetLayout();
} else {
this.$.drawer.open();
this.$.albumLayout.forceNarrow = false;
if (window.innerWidth >= 800) {
this.$.drawer.persistent = true;
}
if (this.mode == "memories") {
this.async(function() {
this.$.calendar.$.days.focus();
}, 100);
}
}
},
listeners: {
"scroll": "onScroll",
"iron-resize" : "onResize"
},
loadPath: function(event) {
this._pathLoad(event.model.item.path);
},
loadAlbum: function(event) {
this._pathLoad(event.detail);
},
_pathLoad: function(path) {
this.path = path;
this.resetPhotos();
this._loadAlbums();
this._loadPhotos();
},
onScroll: function(event) {
if (this.disableScrolling) {
if (event) event.preventDefault();
window.scrollTo(this.topStickX, this.topStickY);
return;
}
if (this.$.bottom.getBoundingClientRect().bottom < window.innerHeight + 200) {
if (this.pendingPhotos.length) {
this.async(this.processItems.bind(this));
}
}
var headerHeight = this.$.header.offsetHeight;
var i, date = null, top = null, lowest = window.innerHeight;
for (i = 0; i < this.visibleThumbs.length; i++) {
var thumb = this.thumbnails[this.visibleThumbs[i]], rect = thumb.getBoundingClientRect();
/* If no thumb is chosen yet, or this one is the highest on the page, use it */
if (rect.top > 0 && (!top || lowest > rect.top)) {
lowest = rect.top
top = thumb;
}
}
if (top) {
var photo = top.item;
this.$.pager.style.opacity = 1;
var date = window.moment(new Date((photo.taken || photo.modified || photo.added).replace(/T.*/, " GMT")));
date = date.calendar(null, { sameElse: "MMM DD, YYYY" }).replace(/ at.*/, "");
this.$.pager.textContent = date;
this.$.pager.style.top = headerHeight +
Math.floor((window.innerHeight - 2 * headerHeight) * (window.scrollY / document.scrollingElement.scrollHeight)) + 'px';
this.debounce("hide-pager", function() {
this.$.pager.style.opacity = 0;
}, 250);
}
this.triggerVisibilityChecks();
},
checkPosition: function(thumbIndex) {
var rect = this.thumbnails[thumbIndex].getBoundingClientRect();
if (rect.top > window.innerHeight) {
return +1;
}
if (rect.top + rect.height < 0) {
return -1;
}
return 0;
},
triggerVisibilityChecks: function() {
this.debounce("hide-or-show", function() {
if (!this.thumbnails || !this.thumbnails.length) {
return;
}
var index, start, stop, length = this.thumbnails.length;
start = 0;
stop = this.thumbnails.length - 1;
/* If there were visible thumbs, use the one from the middle as the starting
* point to check visibility. Otherwise, binary-tree the entire image list */
if (this.visibleThumbs.length) {
index = this.visibleThumbs[this.visibleThumbs.length >> 1];
} else {
index = length >> 1;
}
var pos = this.checkPosition(index), last = -1;
while (pos != 0 && last != index) {
last = index; /* safety escape in case the DOM changed and nothing matches */
if (pos == +1) { /* Checked item was too far down page, so look closer to start */
stop = index - 1;
if (stop < 0) {
console.log("No items in viewport -- all are below");
return;
}
index = start + ((stop - start) >> 1);
} else { /* Checked item was too high up on the page, so look farther */
start = index + 1;
if (start == length) {
console.log("No items in viewport -- all are above");
return;
}
index += (stop - start) >> 1;
}
pos = this.checkPosition(index);
}
if (pos != 0) {
console.log("DOM changed or viewport changed and search would never exit; re-scheduling check")
//this.triggerVisibilityChecks();
return;
}
/* 'index' now points to a thumbnail that is in the viewport; scan
* above and below until a non-in-viewport item is found in each
* direction. That creates the new 'visible' array. */
var visible = [];
visible.push(index);
/* Scan above... */
var tmp = index - 1;
while (tmp >= 0 && this.checkPosition(tmp) == 0) {
visible.push(tmp--);
}
tmp = index + 1;
while (tmp < length && this.checkPosition(tmp) == 0) {
visible.push(tmp++);
}
visible.sort(function(a, b) {
return a - b;
});
var i;
/* Remove 'visible' attribute from any thumbnail
* that was visible and is not in the new set of
* visible thumbs. */
this.visibleThumbs.forEach(function(index) {
for (i = 0; i < visible.length; i++) {
if (visible[i] == index) {
return;
}
}
this.thumbnails[index].visible = false;
}.bind(this));
/* Randomly index the visible array, keeping the center
* in the middle. This makes the loading look more organic. */
for (var i = 0, j = visible.length - 1; i < j; i++, j--) {
if (Math.random() > 0.5) {
var tmp = visible[i];
visible[i] = visible[j];
visible[j] = tmp;
}
}
/* Turn on visibility for any item that is now visible */
visible.forEach(function(index) {
if (this.thumbnails[index].visible != true) {
this.thumbnails[index].visible = true;
}
}.bind(this));
this.visibleThumbs = visible;
});
},
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;
if (this.lightBoxElement) {
this.lightBoxElement.selected = false;
this.updateStyles();
}
},
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);
this.triggerVisibilityChecks();
},
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);
this.triggerVisibilityChecks();
},
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.selected = false;
}
this.$.lightbox.item = el.item;
this.$.lightbox.src = this.base + el.item.path + "thumbs/scaled/" + el.item.filename;
this.lightBoxElement = el;
this.lightBoxElement.selected = true;
this.disableScrolling = true;
this.topStickX = window.scrollX;
this.topStickY = window.scrollY;
this.$.lightbox.open();
this.updateStyles();
},
_imageTap: function(event) {
this.loadLightbox(event.currentTarget);
},
_pathTap: function(event) {
window.location.href = event.model.item.filepath;
},
appendItems: function(photos) {
if (!this.pendingPhotos || !photos || photos.length == 0) {
return;
}
photos.forEach(function(photo) {
var year = (photo.taken || photo.modified || photo.added).replace(/^(....).*$/, "$1");
for (var i = 0; i < this.years.length; i++) {
if (this.years[i] == year) {
return;
}
}
this.push("years", year);
}.bind(this));
this.pendingPhotos = this.pendingPhotos.concat(photos);
this.notifyPath("pendingPhotos.length");
this.async(this.processItems.bind(this));
console.log("Total pending: " + this.pendingPhotos.length);
},
processItems: function() {
if (this.pendingPhotos.length == 0) {
return;
}
var lastPath = null;
var albums = this.querySelectorAll(".album-line");
if (albums.length) {
lastPath = albums[albums.length - 1];
}
var photos = this.pendingPhotos.splice(0, 50);
this.notifyPath("pendingPhotos.length");
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));
try {
datetime = (photo.taken || photo.modified || photo.added).replace(/T.*/, "");
} catch (error) {
console.log(JSON.stringify(photo, null, 2));
throw error;
}
var dateBlock = this.root.querySelector("#date-" + datetime), thumbnails;
if (!dateBlock) {
dateBlock = document.createElement("div");
dateBlock.id = "date-" + datetime;
dateBlock.classList.add("date-line");
dateBlock.classList.add("layout");
dateBlock.classList.add("vertical");
var header = document.createElement("div");
header.classList.add("header");
header.classList.add("layout");
header.classList.add("center");
header.classList.add("horizontal");
var div = document.createElement("div");
div.classList.add("date");
if (this.mode == "memories") {
var ago = window.moment(datetime, "YYYY-MM-DD").fromNow();
ago = ago.charAt(0).toUpperCase() + ago.substr(1);
div.innerHTML = "<b>" + ago + "</b><br><span style='font-size: 0.8em;font-weight: normal'>" + window.moment(datetime, "YYYY-MM-DD").calendar(null, { sameElse: "MMM DD, YYYY" }).replace(/ at.*/, "") + "</span>";
} else {
div.textContent = window.moment(datetime, "YYYY-MM-DD").calendar(null, { sameElse: "MMM DD, YYYY" }).replace(/ at.*/, "");
}
Polymer.dom(dateBlock).appendChild(header);
Polymer.dom(header).appendChild(div);
thumbnails = document.createElement("div");
thumbnails.classList.add("thumbnails");
thumbnails.classList.add("layout");
thumbnails.classList.add("horizontal");
thumbnails.classList.add("wrap");
Polymer.dom(dateBlock).appendChild(thumbnails);
Polymer.dom(this.$.thumbnails).appendChild(dateBlock);
} else {
thumbnails = Polymer.dom(dateBlock).querySelector(".thumbnails");
}
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));
var header = dateBlock.querySelector(".header");
Polymer.dom(header).appendChild(albumBlock);
}
}
Polymer.dom(thumbnails).appendChild(thumbnail);
this.thumbnails.push(thumbnail);
this.notifyPath("thumbnails.length");
}
/* If the viewport is at the bottom when it finishes processing, trigger to load more. */
this.onScroll();
},
pathTapped: function(event) {
this.path = event.currentTarget.path;
Polymer.dom(this.$.thumbnails).innerHTML = "";
this.next = false;
this._loadAlbums();
this._loadPhotos();
},
_loadPhotos: function(start, append, limit) {
if (this.loading == true || this.mode == "login") {
return;
}
this.loading = true;
this.limit = limit || 100;
var params = {
limit: this.limit,
}, 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]);
}
var path = this.path || "", mode = this.mode;
if (mode != "albums") {
path = mode;
if (mode == "time") {
path = "";
} else {
path = "memories/" + (this.date || "");
}
}
var username = this.user ? this.user.username : "";
console.log("Requesting " + this.limit + " photos from " + path);
window.fetch("api/v1/photos/" + path + query, function(path, error, xhr) {
this.loading = false;
if (!this.user) {
return;
}
if ((username != (this.user ? this.user.username : "")) ||
(mode != this.mode) ||
((mode == "albums") && (path != (this.path || ""))) ||
((mode == "memories") && (path != ("memories/" + (this.date || ""))))) {
console.log("Skipping results for old query. Triggering re-fetch of photos for new path or mode.");
this._loadPhotos();
return;
}
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(/\/$/, "") + "/"; /* Make sure there is a trailing slash */
} else {
this.base = "/";
}
console.log(results.items.length + " photos received.");
this.appendItems(results.items);
if (results.more) {
var cursor = results.items[results.items.length - 1];
this._loadPhotos(cursor.taken.toString().replace(/T.*/, "") + "_" + cursor.id, true, this.limit * 2);
}
}.bind(this, path));
},
_loadAlbums: function() {
if (this.loadingAlbums == true || this.mode == "login") {
return;
}
this.loadingAlbums = true;
var path = this.path || "";
window.fetch("api/v1/albums/" + path, function(path, error, xhr) {
this.loadingAlbums = false;
if (!this.user) {
return;
}
if (path != (this.path || "")) {
console.log("Skipping results for old query. Triggering re-fetch of albums for new path.");
this._loadAlbums();
return;
}
if (error) {
console.log("Error loading album: " + (this.path || ""));
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, path));
},
onResize: function(event) {
this.debounce("resize", function() {
this.triggerVisibilityChecks();
var width = 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);
},
userChanged: function(user) {
this.resetPhotos();
this.path = "";
this.mode = "";
if (user) {
this.mode = "memories";
} else {
this.mode = "login";
}
},
ready: function() {
this.resetPhotos();
this.$.calendar.partsHidden = {
"year": true
};
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);
this.loading = true;
window.fetch("api/v1/users", 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 parse authentication response.";
this.$.toast.setAttribute("error", true);
this.$.toast.updateStyles();
this.$.toast.show();
return;
}
if (results && results.username) {
this.user = results;
}
}.bind(this));
this.onResize();
document.addEventListener("scroll", this.onScroll.bind(this));
}
});
});
</script>
</dom-module>
</html>