Started implementing maintainer action buttons

Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2018-10-07 16:26:42 -07:00
parent 97c2d1dd66
commit 77332428e6
3 changed files with 136 additions and 57 deletions

View File

@ -2,6 +2,7 @@
<html> <html>
<head> <head>
<link rel="import" href="../bower_components/polymer/polymer.html"> <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-icon-button/paper-icon-button.html">
<link rel="import" href="../bower_components/iron-icon/iron-icon.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-icons/iron-icons.html">
<link rel="import" href="../bower_components/iron-pages/iron-pages.html"> <link rel="import" href="../bower_components/iron-pages/iron-pages.html">
@ -28,7 +29,7 @@
@apply --photo-thumbnail; @apply --photo-thumbnail;
} }
:host > div { #info {
position: absolute; position: absolute;
padding: 0.5em; padding: 0.5em;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
@ -37,15 +38,29 @@
left: 0px; left: 0px;
right: 0px; right: 0px;
font-size: 0.6em; font-size: 0.6em;
} }
:host(:hover) > div { #actions {
padding: 0.5em;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0;
font-size: 0.6em;
}
:host(:hover) #info,
:host(:hover) #actions {
opacity: 1; opacity: 1;
} }
</style> </style>
<div class="layout vertical start"> <div id="actions" hidden$="[[!actions.length]]" class="horziontal layout center end">
<template is="dom-repeat" items="[[actions]]">
<paper-icon-button on-tap="_fireAction" icon="[[item]]"></paper-icon-button>
</template>
</div>
<div id="info" class="layout vertical start">
<div>[[item.name]] ([[item.id]])</div> <div>[[item.name]] ([[item.id]])</div>
<div>[[item.taken]]</div> <div>[[item.taken]]</div>
<div on-tap="_pathTap">[[item.path]]</div> <div on-tap="_pathTap">[[item.path]]</div>
@ -82,6 +97,12 @@
"thumbChanged(thumbpath, visible)" "thumbChanged(thumbpath, visible)"
], ],
_fireAction: function(event) {
event.stopPropagation();
event.preventDefault();
this.fire("action", event.model.item);
},
thumbChanged: function(thumbpath, visible) { thumbChanged: function(thumbpath, visible) {
if (visible) { if (visible) {
this.style.backgroundImage = "url(" + this.style.backgroundImage = "url(" +

View File

@ -221,6 +221,7 @@
} }
photo-thumbnail { photo-thumbnail {
transition: opacity 0.25s ease-in-out;
--photo-thumbnail: { --photo-thumbnail: {
border: 3px solid white; border: 3px solid white;
}; };
@ -1212,6 +1213,14 @@
var thumbnail = document.createElement("photo-thumbnail"); var thumbnail = document.createElement("photo-thumbnail");
thumbnail.item = photo; thumbnail.item = photo;
if (this.user.maintainer) {
let actions = [ "delete" ];
if (this.mode == "duplicates") {
actions.unshift("text-format");
}
thumbnail.actions = actions;
thumbnail.addEventListener("action", this._imageAction.bind(this));
}
thumbnail.addEventListener("load-image", this._imageTap.bind(this)); thumbnail.addEventListener("load-image", this._imageTap.bind(this));
thumbnail.addEventListener("load-album", this.loadAlbum.bind(this)); thumbnail.addEventListener("load-album", this.loadAlbum.bind(this));
@ -1226,6 +1235,31 @@
this.processing = false; this.processing = false;
}, },
_imageAction: function(event) {
console.log(event.detail);
switch (event.detail) {
case "delete": /* Delete image */
var thumbnail = event.currentTarget;
thumbnail.style.opacity = 0;
thumbnail.style.pointerEvents = "none";
this.async(function(thumbnail) {
thumbnail.parentElement.removeChild(thumbnail);
for (var i = 0; i < this.thumbnails.length; i++) {
if (this.thumbnails[i] == thumbnail) {
console.log("Found thumbnail");
this.thumbnails.splice(i, 1);
this.notifyPath("thumbnails.length");
break;
}
}
}.bind(this, thumbnail), 250);
break;
case "text-format": /* Rename this image */
break;
}
},
pathTapped: function(event) { pathTapped: function(event) {
this.path = event.currentTarget.path; this.path = event.currentTarget.path;
Polymer.dom(this.$.thumbnails).innerHTML = ""; Polymer.dom(this.$.thumbnails).innerHTML = "";

View File

@ -27,10 +27,10 @@ function removeNewerFile(path, fileA, fileB) {
return; return;
} }
if (statsA.mtime > statsB.mtime) { if (statsA.mtime > statsB.mtime) {
console.log("Removing file by moving to 'corrupt':" + fileA); setStatus("Removing file by moving to 'corrupt':" + fileA);
moveCorrupt(path, fileA); moveCorrupt(path, fileA);
} else { } else {
console.log("Removing file by moving to 'corrupt':" + fileB); setStatus("Removing file by moving to 'corrupt':" + fileB);
moveCorrupt(path, fileB); moveCorrupt(path, fileB);
} }
}); });
@ -101,14 +101,14 @@ const exists = function(path) {
} }
function convertRawToJpg(path, file) { function convertRawToJpg(path, file) {
console.log("Converting " + path + file); setStatus("Converting " + path + file);
path = picturesPath + path; path = picturesPath + path;
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
return exists(path + file.replace(rawExtension, ".jpg")).then(function(exist) { return exists(path + file.replace(rawExtension, ".jpg")).then(function(exist) {
if (exist) { if (exist) {
console.log("Skipping already converted file: " + file); setStatus("Skipping already converted file: " + file);
return; return;
} }
@ -133,25 +133,25 @@ function convertRawToJpg(path, file) {
ufraw.on('exit', function(stderr, code, signal) { ufraw.on('exit', function(stderr, code, signal) {
if (signal || code != 0) { if (signal || code != 0) {
let error = "UFRAW for " + path + file + " returned an error: " + code + "\n" + signal + "\n" + stderr.join("\n") + "\n"; let error = "UFRAW for " + path + file + " returned an error: " + code + "\n" + signal + "\n" + stderr.join("\n") + "\n";
console.error(error); setStatus(error, "error");
return moveCorrupt(path, file).then(function() { return moveCorrupt(path, file).then(function() {
console.warn("ufraw failed"); setStatus("ufraw failed", "warn");
return reject(error); return reject(error);
}).catch(function(error) { }).catch(function(error) {
console.warn("moveCorrupt failed"); setStatus("moveCorrupt failed", "warn");
return reject(error); return reject(error);
}); });
} }
return mkdir(path + "raw").then(function() { return mkdir(path + "raw").then(function() {
fs.rename(path + file, path + "raw/" + file, function(err) { fs.rename(path + file, path + "raw/" + file, function(err) {
if (err) { if (err) {
console.error("Unable to move RAW file: " + path + file); setStatus("Unable to move RAW file: " + path + file, "error");
return reject(err); return reject(err);
} }
return resolve(); return resolve();
}); });
}).catch(function(error) { }).catch(function(error) {
console.warn("mkdir failed"); setStatus("mkdir failed", "warn");
return reject(error); return reject(error);
}); });
}.bind(this, ufraw)); }.bind(this, ufraw));
@ -165,13 +165,13 @@ function moveCorrupt(path, file) {
path = picturesPath + path; path = picturesPath + path;
} }
console.warn("Moving corrupt file '" + file + "' to " + path + "corrupt"); setStatus("Moving corrupt file '" + file + "' to " + path + "corrupt", "warn");
return mkdir(path + "corrupt").then(function() { return mkdir(path + "corrupt").then(function() {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
fs.rename(path + file, path + "corrupt/" + file, function(err) { fs.rename(path + file, path + "corrupt/" + file, function(err) {
if (err) { if (err) {
console.error("Unable to move corrupt file: " + path + file); setStatus("Unable to move corrupt file: " + path + file, "error");
return reject(err); return reject(err);
} }
return resolve(); return resolve();
@ -202,7 +202,7 @@ function processBlock(items) {
}); });
let toProcess = processing.length, lastMessage = moment(); let toProcess = processing.length, lastMessage = moment();
console.log("Items to be processed: " + toProcess); setStatus("Items to be processed: " + toProcess);
return Promise.mapSeries(processing, function(asset) { return Promise.mapSeries(processing, function(asset) {
return computeHash(picturesPath + asset.album.path + asset.filename).then(function(hash) { return computeHash(picturesPath + asset.album.path + asset.filename).then(function(hash) {
asset.hash = hash; asset.hash = hash;
@ -222,7 +222,7 @@ function processBlock(items) {
} else if (results[0].hash != asset.hash) { } else if (results[0].hash != asset.hash) {
query = "UPDATE photohashes SET hash=:hash WHERE photoId=:id"; query = "UPDATE photohashes SET hash=:hash WHERE photoId=:id";
} else if (results[0].photoId != asset.id) { } else if (results[0].photoId != asset.id) {
console.log("Duplicate asset: " + setStatus("Duplicate asset: " +
"'" + asset.album.path + asset.filename + "' is a copy of " + "'" + asset.album.path + asset.filename + "' is a copy of " +
"'" + results[0].path + results[0].filename + "'"); "'" + results[0].path + results[0].filename + "'");
duplicates.push(asset); duplicates.push(asset);
@ -293,7 +293,7 @@ function processBlock(items) {
asset.modified = moment(metadata.exif.exif.DateTimeOriginal).format(); asset.modified = moment(metadata.exif.exif.DateTimeOriginal).format();
if (asset.taken == "Invalid date" || asset.taken.replace(/T.*/, "") == "1899-11-30") { if (asset.taken == "Invalid date" || asset.taken.replace(/T.*/, "") == "1899-11-30") {
console.log("Invalid EXIF date information for " + asset.album.path + asset.filename); setStatus("Invalid EXIF date information for " + asset.album.path + asset.filename);
asset.taken = asset.modified = moment(created).format(); asset.taken = asset.modified = moment(created).format();
} }
} else { } else {
@ -332,7 +332,7 @@ function processBlock(items) {
} }
return image.resize(256, 256).toFile(dst).catch(function(error) { return image.resize(256, 256).toFile(dst).catch(function(error) {
console.error("Error resizing image: " + dst, error); setStatus("Error resizing image: " + dst + "\n" + error, "error");
throw error; throw error;
}); });
}).then(function() { }).then(function() {
@ -343,7 +343,7 @@ function processBlock(items) {
} }
return image.resize(Math.min(1024, metadata.width)).toFile(dst).catch(function(error) { return image.resize(Math.min(1024, metadata.width)).toFile(dst).catch(function(error) {
console.error("Error resizing image: " + dst, error); setStatus("Error resizing image: " + dst + "\n" + error, "error");
throw error; throw error;
}); });
}); });
@ -355,7 +355,7 @@ function processBlock(items) {
}); });
}); });
}).catch(function(error) { }).catch(function(error) {
console.error("Error reading image " + src + ": ", error); setStatus("Error reading image " + src + ":\n" + error, "error");
return moveCorrupt(path, file).then(function() { return moveCorrupt(path, file).then(function() {
/* If the original file was not a NEF/ORF, we are done... */ /* If the original file was not a NEF/ORF, we are done... */
@ -369,31 +369,31 @@ function processBlock(items) {
/* Move the NEF/ORF to /corrupt as well so it isn't re-checked again and again... */ /* Move the NEF/ORF to /corrupt as well so it isn't re-checked again and again... */
// return moveCorrupt(path, asset.filename); // return moveCorrupt(path, asset.filename);
console.error("Already attempted to convert NEF/ORF to JPG: " + path + file); setStatus("Already attempted to convert NEF/ORF to JPG: " + path + file, "error");
return; return;
} }
} }
console.warn("Adding " + path + file + " back onto processing queue."); setStatus("Adding " + path + file + " back onto processing queue.", "error");
triedClean.push(path + file); triedClean.push(path + file);
processBlock([ path, file, created, albumId ]); processBlock([ path, file, created, albumId ]);
}); });
}); });
}).catch(function() { }).catch(function() {
console.warn("Continuing file processing."); setStatus("Continuing file processing.", "warn");
}); });
}).then(function() { }).then(function() {
toProcess--; toProcess--;
if (moment().add(-5, 'seconds') > lastMessage) { if (moment().add(-5, 'seconds') > lastMessage) {
console.log("Items to be processed: " + toProcess); setStatus("Items to be processed: " + toProcess);
lastMessage = moment(); lastMessage = moment();
} }
}); });
}).catch(function(error) { }).catch(function(error) {
console.log("Error processing file. Continuing."); setStatus("Error processing file. Continuing.");
throw error; throw error;
}).then(function() { }).then(function() {
console.log("Completed processing queue. Marking " + duplicates.length + " duplicates."); setStatus("Completed processing queue. Marking " + duplicates.length + " duplicates.");
let dups = []; let dups = [];
duplicates.forEach(function(asset) { duplicates.forEach(function(asset) {
/* If not already marked as a duplicate, mark it. */ /* If not already marked as a duplicate, mark it. */
@ -412,7 +412,7 @@ function processBlock(items) {
} }
}); });
}).then(function() { }).then(function() {
console.log("Looking for removed assets"); setStatus("Looking for removed assets");
return photoDB.sequelize.query("SELECT photos.scanned,photos.id,photos.filename,albums.path FROM photos " + return photoDB.sequelize.query("SELECT photos.scanned,photos.id,photos.filename,albums.path FROM photos " +
"LEFT JOIN albums ON (albums.id=photos.albumId) " + "LEFT JOIN albums ON (albums.id=photos.albumId) " +
"WHERE photos.deleted=0 AND DATETIME(photos.scanned)<DATETIME(:lastScan)", { "WHERE photos.deleted=0 AND DATETIME(photos.scanned)<DATETIME(:lastScan)", {
@ -422,11 +422,11 @@ function processBlock(items) {
type: photoDB.sequelize.QueryTypes.SELECT type: photoDB.sequelize.QueryTypes.SELECT
}).then(function(results) { }).then(function(results) {
let deleted = []; let deleted = [];
console.log("Checking " + results.length + " assets to see if they are on disk."); setStatus("Checking " + results.length + " assets to see if they are on disk.");
return Promise.map(results, function(asset) { return Promise.map(results, function(asset) {
return exists(asset.path + asset.filename).then(function(exist) { return exists(asset.path + asset.filename).then(function(exist) {
if (!exist) { if (!exist) {
console.log(asset.path + asset.filename + " no longer exists on disk. Marking as deleted."); setStatus(asset.path + asset.filename + " no longer exists on disk. Marking as deleted.");
deleted.push(asset.id); deleted.push(asset.id);
} }
}); });
@ -442,7 +442,7 @@ function processBlock(items) {
} }
}); });
}).then(function() { }).then(function() {
console.log(deleted.length + " assets deleted."); setStatus(deleted.length + " assets deleted.");
}); });
}); });
}); });
@ -464,7 +464,7 @@ function scanDir(parent, path) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
fs.readdir(path, function(error, files) { fs.readdir(path, function(error, files) {
if (error) { if (error) {
console.warn("Could not readdir: " + path); setStatus("Could not readdir: " + path, "warn");
return resolve([]); return resolve([]);
} }
@ -480,7 +480,7 @@ function scanDir(parent, path) {
* version from disk. */ * version from disk. */
if (file != files[i] && file.toUpperCase() == files[i]) { if (file != files[i] && file.toUpperCase() == files[i]) {
removeNewerFile(path, file, files[i]); removeNewerFile(path, file, files[i]);
console.log("Duplicate file in " + path + ": ", file, files[i]); setStatus("Duplicate file in " + path + ": ", file, files[i]);
return false; return false;
} }
} }
@ -502,7 +502,7 @@ function scanDir(parent, path) {
albums = albums.concat(_albums); albums = albums.concat(_albums);
assets = assets.concat(_assets); assets = assets.concat(_assets);
}).catch(function(error) { }).catch(function(error) {
console.warn("Could not scanDir " + filepath + ": " + error); setStatus("Could not scanDir " + filepath + ": " + error, "error");
}); });
} }
@ -544,7 +544,7 @@ function findOrCreateDBAlbum(transaction, album) {
} else { } else {
if (!album.parent.id) { if (!album.parent.id) {
let error = "Albums in array in non ancestral order!"; let error = "Albums in array in non ancestral order!";
console.error(error); setStatus(error, "error");
throw error; throw error;
} }
album.parentId = album.parent.id; album.parentId = album.parent.id;
@ -557,7 +557,7 @@ function findOrCreateDBAlbum(transaction, album) {
}).then(function(results) { }).then(function(results) {
if (results.length == 0) { if (results.length == 0) {
if (!album.parent) { if (!album.parent) {
console.warn("Creating top level album: " + picturesPath); setStatus("Creating top level album: " + picturesPath, "warn" );
} }
return photoDB.sequelize.query("INSERT INTO albums (path,parentId,name) VALUES(:path,:parentId,:name)", { return photoDB.sequelize.query("INSERT INTO albums (path,parentId,name) VALUES(:path,:parentId,:name)", {
replacements: album, replacements: album,
@ -577,7 +577,7 @@ function findOrCreateDBAlbum(transaction, album) {
function findOrUpdateDBAsset(transaction, asset) { function findOrUpdateDBAsset(transaction, asset) {
if (!asset.album || !asset.album.id) { if (!asset.album || !asset.album.id) {
let error = "Asset being processed without an album"; let error = "Asset being processed without an album";
console.error(error); setStatus(error, "warn");
throw error; throw error;
} }
@ -607,7 +607,7 @@ function findOrUpdateDBAsset(transaction, asset) {
/* If the size on disk changed, update the size entry in the DB. This shouldn't happen in /* If the size on disk changed, update the size entry in the DB. This shouldn't happen in
* production unless someone modifies the file, then re-stamps the modified time */ * production unless someone modifies the file, then re-stamps the modified time */
if (asset.size != results[0].size) { if (asset.size != results[0].size) {
console.log("File was modified with time-restamp (HASH regeneration will be queued): " + asset.filename); setStatus("File was modified with time-restamp (HASH regeneration will be queued): " + asset.filename);
delete asset.scanned; delete asset.scanned;
delete asset.modified; delete asset.modified;
} }
@ -638,7 +638,30 @@ function computeHash(filepath) {
}); });
} }
let scanning = false; let scanningStatus = [];
function setStatus(status, level) {
if (status == "idle") {
scanningStatus = [];
}
level = level || "info";
scanningStatus.push({
level: level,
time: moment().format(),
log: status
});
switch (level) {
case "error":
console.error(status);
break;
case "warn":
console.warn(status);
break;
default:
console.log(status);
}
}
function doScan() { function doScan() {
/* 1. Scan for all assets which will be managed by the system. readdir /* 1. Scan for all assets which will be managed by the system. readdir
* 2. Check if entry in DB. Check mod-time in DB vs. stats from #1 * 2. Check if entry in DB. Check mod-time in DB vs. stats from #1
@ -659,12 +682,12 @@ function doScan() {
let now = Date.now(); let now = Date.now();
let needsProcessing = []; let needsProcessing = [];
if (scanning) { if (scanningStatus.length != 0) {
return Promise.resolve("scanning"); return Promise.resolve(scanningStatus);
} }
return scanDir(null, picturesPath).spread(function(albums, assets) { return scanDir(null, picturesPath).spread(function(albums, assets) {
console.log("Found " + assets.length + " assets in " + albums.length + " albums after " + setStatus("Found " + assets.length + " assets in " + albums.length + " albums after " +
((Date.now() - now) / 1000) + "s"); ((Date.now() - now) / 1000) + "s");
/* One at a time, in series, as the album[] array has parents first, then descendants. /* One at a time, in series, as the album[] array has parents first, then descendants.
* Operating in parallel could result in a child being searched for prior to the parent */ * Operating in parallel could result in a child being searched for prior to the parent */
@ -676,17 +699,17 @@ function doScan() {
return findOrCreateDBAlbum(transaction, album).then(function() { return findOrCreateDBAlbum(transaction, album).then(function() {
toProcess--; toProcess--;
if (moment().add(-5, 'seconds') > lastMessage) { if (moment().add(-5, 'seconds') > lastMessage) {
console.log("Albums to be created in DB: " + toProcess); setStatus("Albums to be created in DB: " + toProcess);
lastMessage = moment(); lastMessage = moment();
} }
}); });
}); });
}).then(function() { }).then(function() {
console.log("Processed " + albums.length + " album DB entries in " + setStatus("Processed " + albums.length + " album DB entries in " +
((Date.now() - now) / 1000) + "s"); ((Date.now() - now) / 1000) + "s");
now = Date.now(); now = Date.now();
console.log(assets.length + " assets remaining to be verified/updated. ETA N/A"); setStatus(assets.length + " assets remaining to be verified/updated. ETA N/A");
let processed = 0, start = Date.now(), last = 0, updateScanned = [], newEntries = 0; let processed = 0, start = Date.now(), last = 0, updateScanned = [], newEntries = 0;
return photoDB.sequelize.transaction(function(transaction) { return photoDB.sequelize.transaction(function(transaction) {
@ -721,7 +744,7 @@ function doScan() {
} }
let remaining = assets.length - processed, eta = Math.ceil((elapsed / 1000) * remaining / (processed - last)); let remaining = assets.length - processed, eta = Math.ceil((elapsed / 1000) * remaining / (processed - last));
console.log(remaining + " assets remaining be verified/updated " + setStatus(remaining + " assets remaining be verified/updated " +
"(" + newEntries + " new entries, " + (processed - newEntries) + " up-to-date so far). ETA " + eta + "s"); "(" + newEntries + " new entries, " + (processed - newEntries) + " up-to-date so far). ETA " + eta + "s");
last = processed; last = processed;
start = Date.now(); start = Date.now();
@ -736,23 +759,23 @@ function doScan() {
ids: updateScanned ids: updateScanned
} }
}).then(function() { }).then(function() {
console.log("Updated scan date of " + updateScanned.length + " assets"); setStatus("Updated scan date of " + updateScanned.length + " assets");
updateScanned = []; updateScanned = [];
}); });
} }
}).then(function() { }).then(function() {
console.log(newEntries + " assets are new. " + (needsProcessing.length - newEntries) + " assets have been modified."); setStatus(newEntries + " assets are new. " + (needsProcessing.length - newEntries) + " assets have been modified.\n" +
console.log(needsProcessing.length + " assets need HASH computed. " + (assets.length - needsProcessing.length) + " need no update.");; needsProcessing.length + " assets need HASH computed. " + (assets.length - needsProcessing.length) + " need no update.");;
processBlock(needsProcessing); processBlock(needsProcessing);
needsProcessing = []; needsProcessing = [];
}).then(function() { }).then(function() {
console.log("Scanned " + assets.length + " asset DB entries in " + setStatus("Scanned " + assets.length + " asset DB entries in " +
((Date.now() - now) / 1000) + "s"); ((Date.now() - now) / 1000) + "s");
assets = []; assets = [];
}); });
}); });
}).then(function() { }).then(function() {
console.log("Total time to initialize DB and all scans: " + ((Date.now() - initialized) / 1000) + "s"); setStatus("Total time to initialize DB and all scans: " + ((Date.now() - initialized) / 1000) + "s");
return photoDB.sequelize.query("SELECT max(scanned) AS scanned FROM photos", { return photoDB.sequelize.query("SELECT max(scanned) AS scanned FROM photos", {
type: photoDB.sequelize.QueryTypes.SELECT type: photoDB.sequelize.QueryTypes.SELECT
}).then(function(results) { }).then(function(results) {
@ -761,11 +784,14 @@ function doScan() {
} else { } else {
lastScan = new Date(results[0].scanned); lastScan = new Date(results[0].scanned);
} }
console.log("Updating any asset newer than " + moment(lastScan).format()); setStatus("Updating any asset newer than " + moment(lastScan).format());
}); });
}).then(function() { }).then(function() {
scanning = false; setStatus("idle");
return "scan complete"; return "scan complete";
}).catch(function(error) {
setStatus(error);
throw error;
}); });
} }
@ -773,8 +799,6 @@ module.exports = {
init: function(db) { init: function(db) {
photoDB = db; photoDB = db;
}, },
scan: function () { scan: doScan
return doScan();
}
}; };