Convert Gallery to work with PouchType

This commit is contained in:
Timothy Farrell 2018-07-15 23:05:09 -05:00
parent 797b0960e6
commit d32eb0d728
12 changed files with 81 additions and 162 deletions

View File

@ -23,7 +23,7 @@
"pouchdb-core": "~7.0.0", "pouchdb-core": "~7.0.0",
"pouchdb-find": "~7.0.0", "pouchdb-find": "~7.0.0",
"pouchdb-replication": "~7.0.0", "pouchdb-replication": "~7.0.0",
"pouchorm": "~1.0.0", "pouchtype": "~1.0.0",
"request": "~2.87.0", "request": "~2.87.0",
"router": "2.1.0", "router": "2.1.0",
"semantic-ui-reset": "~2.2.12", "semantic-ui-reset": "~2.2.12",

View File

@ -1,6 +1,7 @@
import pica from 'pica/dist/pica'; import pica from 'pica/dist/pica';
import { FileType } from '../data/file.js'; import { FileType } from '../data/file.js';
import { ImageType } from '../data/image.js';
const THUMBNAIL_MAX_DIMENSION = 320; const THUMBNAIL_MAX_DIMENSION = 320;
@ -60,7 +61,7 @@ export async function generateThumbnailForImage(doc) {
const mimetype = attachment.type; const mimetype = attachment.type;
const resizedBlob = await resizeImage(attachment, mimetype, width, height); const resizedBlob = await resizeImage(attachment, mimetype, width, height);
const thumbfile = await FileType.upload(resizedBlob); const thumbfile = await FileType.upload(resizedBlob);
await doc.update({ await ImageType.update(doc, {
sizes: { sizes: {
thumbnail: FileType.getURL(thumbfile) thumbnail: FileType.getURL(thumbfile)
} }
@ -68,7 +69,7 @@ export async function generateThumbnailForImage(doc) {
} else { } else {
console.log('using original as thumbnail'); console.log('using original as thumbnail');
// Thumbnail would be bigger so let's just use the original. // Thumbnail would be bigger so let's just use the original.
await doc.update({ await ImageType.update(doc, {
sizes: { sizes: {
thumbnail: doc.sizes.full thumbnail: doc.sizes.full
} }

View File

@ -1,85 +0,0 @@
import { TypeSpec } from 'pouchorm';
import { PouchDB, db } from '../services/db.js';
import { ImageType } from '../data/image.js';
import { extractID } from '../utils/conversion.js';
class AlbumSpec extends TypeSpec {
static getUniqueID(doc) {
return doc.title
.trim()
.replace(/[ \-~!@#$%^&]/g, '_')
.toLowerCase();
}
async findImages(live = false) {
return await ImageType.find(
Object.assign({ [`$$links.${this._id}`]: { $exists: true } }, ImageType.selector),
live
);
}
async addImage(image) {
if (!image.$$links[this._id]) {
await image.update({
$$links: {
[this._id]: {
sequence: this.count
}
}
});
this.count += 1;
await this.save();
}
return image;
}
async removeImage(image) {
if (image.$$links[this._id]) {
delete image.$$links[this._id];
this.count -= 1;
await image.save();
await this.save();
}
return image;
}
async addImageBlob(blob) {
return await this.addImage(await ImageType.upload(blob));
}
async delete(cascade = true) {
if (cascade) {
const images = await this.findImages();
images.map(async i => await this.removeImage(i));
}
return await this.update({ _deleted: true });
}
//
// static validate(doc) {
// // TODO actually validate perhaps against a JSON schema
//
// const schema = {
// title: t.REQUIRED_STRING,
// createdDate: t.REQUIRED_DATE,
// count: t.REQUIRED_INTEGER
// };
// }
}
export const AlbumType = PouchDB.registerType('Album', AlbumSpec, db);
ImageType.subscribe((id, deleted, doc) => {
if (!deleted) {
return;
}
Object.keys(doc.$$links)
.filter(k => k.startsWith(AlbumType.prefix))
.forEach(async albumId => {
const album = await AlbumType.find(albumId);
album.count -= 1;
album.save();
});
});

View File

@ -1,43 +1,43 @@
import { TypeSpec } from 'pouchorm'; import { TypeHandler } from 'pouchtype';
import { sha1 } from '../utils/crypto.js'; import { sha1 } from '../utils/crypto.js';
import { blobToArrayBuffer } from '../utils/conversion.js'; import { blobToArrayBuffer } from '../utils/conversion.js';
import { PouchDB, db } from '../services/db.js'; import { PouchDB, db } from '../services/db.js';
class FileSpec extends TypeSpec { class FileHandler extends TypeHandler {
static getUniqueID(doc) { getUniqueID(doc) {
return doc.digest.substr(0, 16); return doc.digest.substr(0, 16);
} }
static getURL(doc, attachmentName = 'data') { getURL(doc, attachmentName = 'data') {
const end = attachmentName ? '/' + attachmentName : ''; const end = attachmentName ? '/' + attachmentName : '';
return `/${doc._prefix}/${doc._id}` + end; return `/${doc.type}/${doc._id}` + end;
} }
static async getDocFromURL(path) { async getDocFromURL(path) {
if (path.endsWith('/')) { if (path.endsWith('/')) {
path = path.substr(0, path.length - 1); path = path.substr(0, path.length - 1);
} }
const [_, db, id, attname] = path.split('/'); const [_, db, id, attname] = path.split('/');
return await FileType.find(id); return await this.get(id);
} }
static async getFromURL(path) { async getFromURL(path) {
if (path.endsWith('/')) { if (path.endsWith('/')) {
path = path.substr(0, path.length - 1); path = path.substr(0, path.length - 1);
} }
const [_, db, id, attname] = path.split('/'); const [_, db, id, attname] = path.split('/');
const doc = await FileType.find(id); const doc = await this.get(id);
if (attname) { if (attname) {
return await doc.getAttachment(attname); return await this.getAttachment(doc, attname);
} }
return doc; return doc;
} }
static async upload(blob) { async upload(blob) {
const digest = await sha1(await blobToArrayBuffer(blob)); const digest = await sha1(await blobToArrayBuffer(blob));
const lastModified = blob.lastModified ? new Date(blob.lastModified) : new Date(); const lastModified = blob.lastModified ? new Date(blob.lastModified) : new Date();
return await FileType.getOrCreate({ return await this.getOrCreate({
name: blob.name, name: blob.name,
mimetype: blob.type, mimetype: blob.type,
size: blob.size, size: blob.size,
@ -73,4 +73,4 @@ class FileSpec extends TypeSpec {
// } // }
} }
export const FileType = PouchDB.registerType('File', FileSpec, db); export const FileType = new FileHandler(db, 'file');

View File

@ -1,15 +1,15 @@
import { TypeSpec } from 'pouchorm'; import { TypeHandler } from 'pouchtype';
import { PouchDB, db } from '../services/db.js'; import { db } from '../services/db.js';
import { blobToArrayBuffer, deepAssign } from '../utils/conversion.js'; import { blobToArrayBuffer, deepAssign } from '../utils/conversion.js';
import { backgroundTask } from '../utils/event.js'; import { backgroundTask } from '../utils/event.js';
import { FileType } from './file.js'; import { FileType } from './file.js';
import { error } from '../utils/console.js'; import { error } from '../utils/console.js';
class ImageSpec extends TypeSpec { class ImageHandler extends TypeHandler {
static async upload(blob) { async upload(blob) {
const f = await FileType.upload(blob, false); const f = await FileType.upload(blob, false);
const doc = await ImageType.getOrCreate({ const doc = await this.getOrCreate({
digest: f.digest, digest: f.digest,
originalDate: f.lastModified, originalDate: f.lastModified,
importing: true, importing: true,
@ -23,18 +23,19 @@ class ImageSpec extends TypeSpec {
return doc; return doc;
} }
static getUniqueID(doc) { getUniqueID(doc) {
return doc.digest.substr(0, 16); return doc.digest.substr(0, 16);
} }
async delete(cascade = true) { async remove(docOrId, cascade = true) {
if (cascade) { if (cascade) {
new Set(Object.keys(this.sizes)).forEach(async key => { const doc = typeof docOrId === 'string' ? await this.get(docOrId) : docOrId;
const f = await FileType.getDocFromURL(this.sizes[key]);
f.delete(); Object.values(doc.sizes).forEach(async link => {
await FileType.remove(await FileType.getDocFromURL(link));
}); });
} }
return await this.update({ _deleted: true }); return await super.remove(docOrId);
} }
// //
// static validate(doc) { // static validate(doc) {
@ -79,10 +80,12 @@ const processImportables = backgroundTask(async function _processImportables(ima
const imageData = await FileType.getFromURL(sizes.full); const imageData = await FileType.getFromURL(sizes.full);
const img = new Image(); const img = new Image();
const imageProps = await new Promise(resolve => { await new Promise(resolve => {
img.onload = () => { img.onload = () => {
resolve({ width: img.width, height: img.height }); image.width = img.width;
image.height = img.height;
URL.revokeObjectURL(img.src); URL.revokeObjectURL(img.src);
resolve();
}; };
img.src = URL.createObjectURL(imageData); img.src = URL.createObjectURL(imageData);
}); });
@ -97,33 +100,36 @@ const processImportables = backgroundTask(async function _processImportables(ima
tags.DateTimeOriginal ? new Date(tags.DateTimeOriginal * 1000).toISOString() : image.originalDate tags.DateTimeOriginal ? new Date(tags.DateTimeOriginal * 1000).toISOString() : image.originalDate
).toISOString(); ).toISOString();
deepAssign(imageProps, { ImageType.update(
originalDate, image,
orientation: tags.Orientation, {
digest, originalDate,
make: tags.Make, orientation: tags.Orientation,
model: tags.Model, digest,
flash: !!tags.Flash, make: tags.Make,
iso: tags.ISO, model: tags.Model,
sizes, flash: !!tags.Flash,
gps: { iso: tags.ISO,
latitude: tags.GPSLatitude, sizes,
longitude: tags.GPSLongitude, gps: {
altitude: tags.GPSAltitude, latitude: tags.GPSLatitude,
heading: tags.GPSImgDirection longitude: tags.GPSLongitude,
} altitude: tags.GPSAltitude,
}); heading: tags.GPSImgDirection
}
},
false
);
} }
delete image.importing; ImageType.update(image, { importing: undefined });
image.update(imageProps);
const module = await import('../context/generateThumbnails'); const module = await import('../context/generateThumbnails');
module.generateThumbnailForImage(image); module.generateThumbnailForImage(image);
}, false); }, false);
export const ImageType = PouchDB.registerType('Image', ImageSpec, db); export const ImageType = new ImageHandler(db, 'image');
ImageType.index('originalDate', ['originalDate', 'id']); ImageType.index('originalDate', ['originalDate']);
ImageType.find({ importing: true }).then(results => results.forEach(processImportables)); ImageType.filter({ importing: true }).then(results => results.forEach(processImportables));

View File

@ -22,7 +22,7 @@ import { injectStyle, styled } from '../utils/style.js';
import { CLICKABLE } from './styles.js'; import { CLICKABLE } from './styles.js';
export function uploadImages(evt, files) { export function uploadImages(evt, files) {
Array.from(files || evt.currentTarget.files).forEach(ImageType.upload); Array.from(files || evt.currentTarget.files).forEach(ImageType.upload.bind(ImageType));
if (evt.currentTarget) { if (evt.currentTarget) {
evt.currentTarget.value = null; evt.currentTarget.value = null;
@ -120,7 +120,7 @@ export function AllImagesView(vm, params) {
function deleteSelectedImages() { function deleteSelectedImages() {
if (confirm(`Delete ${selectedIds.size} image(s)?`)) { if (confirm(`Delete ${selectedIds.size} image(s)?`)) {
selectedIds.forEach(ImageType.delete); selectedIds.forEach(ImageType.remove.bind(ImageType));
selectedIds.clear(); selectedIds.clear();
} }
} }
@ -158,12 +158,10 @@ export function AllImagesView(vm, params) {
containerScrollTop(evt.target.scrollTop); containerScrollTop(evt.target.scrollTop);
} }
ImageType.find( ImageType.watch({
{ ['sizes.thumbnail']: { $exists: true }
['sizes.thumbnail']: { $exists: true } }).then(la => {
}, const injectImages = imgArr => images.splice(0, images.length, ...imgArr);
{ live: true }
).then(la => {
subscribeToRender(vm, [ subscribeToRender(vm, [
selectedIds, selectedIds,
images, images,
@ -171,8 +169,9 @@ export function AllImagesView(vm, params) {
appBarStyle, appBarStyle,
hover, hover,
hoverSelectButton, hoverSelectButton,
() => la.subscribe(res => images.splice(0, images.length, ...res)) () => la.subscribe(injectImages)
]); ]);
injectImages(la());
}); });
function renderSection({ title, sectionId, images: _images }) { function renderSection({ title, sectionId, images: _images }) {

View File

@ -67,7 +67,7 @@ export function FocusView(vm, params) {
async function clickTrash() { async function clickTrash() {
if (confirm('Delete this image?')) { if (confirm('Delete this image?')) {
await ImageType.delete(id()); await ImageType.remove(id());
navBack(); navBack();
} }
} }
@ -105,9 +105,9 @@ export function FocusView(vm, params) {
if (!_id) { if (!_id) {
return; return;
} }
doc(await ImageType.find(_id)); doc(await ImageType.get(_id));
const n = await ImageType.find( const n = await ImageType.filter(
{ {
originalDate: { $gte: doc().originalDate } originalDate: { $gte: doc().originalDate }
}, },
@ -119,7 +119,7 @@ export function FocusView(vm, params) {
} }
); );
const p = await ImageType.find( const p = await ImageType.filter(
{ {
originalDate: { $lte: doc().originalDate } originalDate: { $lte: doc().originalDate }
}, },

View File

@ -8,7 +8,6 @@ import {
defineElement as el defineElement as el
} from '../utils/domvm.js'; } from '../utils/domvm.js';
import { ImageType } from '../data/image.js'; import { ImageType } from '../data/image.js';
import { AlbumType } from '../data/album.js';
import { AllImagesView, uploadImages } from './allImages.js'; import { AllImagesView, uploadImages } from './allImages.js';
import { FocusView } from './focus.js'; import { FocusView } from './focus.js';
import { Dropzone } from './components/dropzone.js'; import { Dropzone } from './components/dropzone.js';

View File

@ -92,7 +92,7 @@ export function SectionView(vm, params, key) {
onmouseenter: [patchRefStyle, sectionSelectButtonRef, 'opacity: 0.7;'], onmouseenter: [patchRefStyle, sectionSelectButtonRef, 'opacity: 0.7;'],
onmouseleave: [patchRefStyle, sectionSelectButtonRef, 'opacity: 0;'], onmouseleave: [patchRefStyle, sectionSelectButtonRef, 'opacity: 0;'],
_data: { _data: {
$$type: 'section', type: 'section',
sectionImageIds: photos.map(extractID) sectionImageIds: photos.map(extractID)
} }
}, },

View File

@ -4,22 +4,21 @@ import http from 'pouchdb-adapter-http';
import replication from 'pouchdb-replication'; import replication from 'pouchdb-replication';
import find from 'pouchdb-find'; import find from 'pouchdb-find';
import { PouchORM } from 'pouchorm'; import { PouchType } from 'pouchtype';
import { B2Adapter } from './b2.js'; import { B2Adapter } from './b2.js';
export const PouchDB = core export const PouchDB = core
.plugin(idb) .plugin(idb)
.plugin(http) .plugin(http)
.plugin(replication) .plugin(replication)
.plugin(find) .plugin(find);
// .plugin( // .plugin(
// B2Adapter( // B2Adapter(
// B2_ACCOUNT, // B2_ACCOUNT,
// B2_API_KEY, // B2_API_KEY,
// B2_BUCKET_ID // B2_BUCKET_ID
// ) // )
// ) // );
.plugin(PouchORM);
export const db = new PouchDB('gallery'); export const db = new PouchDB('gallery');

View File

@ -42,7 +42,7 @@ export function PouchDBAttachmentProxy({ save, getFn, remove }) {
// All documents must have a .name field. // All documents must have a .name field.
const deletedFiles = []; const deletedFiles = [];
const attachments = []; const attachments = [];
docs.filter(d => d.$$type === 'file').forEach(f => { docs.filter(d => d.type === 'file').forEach(f => {
if (f._deleted) { if (f._deleted) {
deletedFiles.push(pouchGetAttachment.call(this, f._id, 'data')); deletedFiles.push(pouchGetAttachment.call(this, f._id, 'data'));
return; return;

View File

@ -54,7 +54,7 @@ export function changeElementStateMap(refStateMap, evt, node, vm) {
export function nodeParentWithType(node, type) { export function nodeParentWithType(node, type) {
let parentNode = node; let parentNode = node;
while (parentNode && (!parentNode.data || parentNode.data.$$type !== type)) { while (parentNode && (!parentNode.data || parentNode.data.type !== type)) {
parentNode = parentNode.parent; parentNode = parentNode.parent;
} }
if (!parentNode) { if (!parentNode) {