From 722944caddf13ee1523bc7f1d6973b6ac968b2ae Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Mon, 30 Oct 2017 04:22:21 -0500 Subject: [PATCH] Interface is fully DB-driven While there are still events for things like maintaining indexes, those may be unnecessary and may go away. --- packages/gallery/package.json | 1 + packages/gallery/src/app.js | 50 ++++------------- packages/gallery/src/data/image.js | 6 ++- packages/gallery/src/data/indexType.js | 6 +++ packages/gallery/src/interface/album.js | 66 +++++++++++++++++++---- packages/gallery/src/interface/gallery.js | 44 ++++++++++++--- packages/gallery/src/interface/image.js | 33 +++++++++--- packages/gallery/src/utils/livearray.js | 35 ++++++++++++ packages/gallery/src/utils/watcher.js | 2 +- 9 files changed, 177 insertions(+), 66 deletions(-) create mode 100644 packages/gallery/src/utils/livearray.js diff --git a/packages/gallery/package.json b/packages/gallery/package.json index 66a25ee..ced2901 100644 --- a/packages/gallery/package.json +++ b/packages/gallery/package.json @@ -14,6 +14,7 @@ "dependencies": { "domvm": "~3.2.0", "exif-parser": "~0.1.9", + "frptools": "1.1.0", "pica": "~2.0.8", "pouchdb-adapter-http": "~6.3.4", "pouchdb-adapter-idb": "~6.3.4", diff --git a/packages/gallery/src/app.js b/packages/gallery/src/app.js index 995f353..2c3b1cb 100644 --- a/packages/gallery/src/app.js +++ b/packages/gallery/src/app.js @@ -1,50 +1,22 @@ -import { createView } from 'domvm/dist/dev/domvm.dev.js'; +// import { createView } from 'domvm/dist/dev/domvm.dev.js'; +import { createView } from 'domvm/dist/full/domvm.full.js'; import * as image from './data/image.js'; -import * as index from './data/indexType.js'; -import { getDatabase } from './services/db.js'; -import * as imageTag from './context/manageImageTags.js'; import generateThumbnails from './contextLoaders/generateThumbnails.js'; import { GalleryView } from './interface/gallery.js'; -import { router, routeChanged } from './services/router.js'; +import { router } from './services/router.js'; +import { getDatabase } from './services/db.js'; +import { EventEmitter } from 'events'; + +EventEmitter.defaultMaxListeners = 1000; // https://github.com/pouchdb/pouchdb/issues/6123 window.db = getDatabase(); -const NAV_OPTIONS = { - images: { - model: image, - title: 'Images' - }, - albums: { - model: index, - title: 'Albums' - } -}; - -async function update(route) { - const o = NAV_OPTIONS[route.name]; - gallery.update({ - title: o.title, - members: (await o.model.find({ attachments: true })).rows - }); -} -function redraw() { - update(router.current()); -} -function onRouteChange(router, route) { - update(route); -} - +// Watch for new images, generate thumbnails if they need them. image.watcher(generateThumbnails); -image.imported.subscribe(redraw); -image.removed.subscribe(redraw); -index.added.subscribe(redraw); -index.removed.subscribe(redraw); -routeChanged.subscribe(onRouteChange); -const gallery = createView(GalleryView, { - title: '', - members: [] -}).mount(document.querySelector('#app')); +// Attach our root view to the DOM +createView(GalleryView, {}).mount(document.querySelector('#app')); +// Start the router router.start('home'); diff --git a/packages/gallery/src/data/image.js b/packages/gallery/src/data/image.js index a77d5e0..a695648 100644 --- a/packages/gallery/src/data/image.js +++ b/packages/gallery/src/data/image.js @@ -8,7 +8,7 @@ import { Watcher } from '../utils/watcher.js'; const db = getDatabase(); const PROCESS_PREFIX = 'importing'; const PREFIX = 'image'; -const SELECTOR = { +export const SELECTOR = { _id: { $gt: `${PREFIX}_`, $lt: `${PREFIX}_\ufff0` @@ -45,6 +45,10 @@ export async function find(keys, options = {}) { return await db.allDocs(opts); } +export async function getAttachment(id, attName) { + return await db.getAttachment(id, attName); +} + export async function add(imageFileList) { const docs = Array.prototype.map.call(imageFileList, f => ({ _id: `${PROCESS_PREFIX}_${f.name}`, diff --git a/packages/gallery/src/data/indexType.js b/packages/gallery/src/data/indexType.js index 0482a90..562f815 100644 --- a/packages/gallery/src/data/indexType.js +++ b/packages/gallery/src/data/indexType.js @@ -4,6 +4,12 @@ import { Event } from '../utils/event.js'; const db = getDatabase(); const PREFIX = 'index'; +export const SELECTOR = { + _id: { + $gt: `${PREFIX}_`, + $lt: `${PREFIX}_\ufff0` + } +}; // Events export const added = new Event('Index.added'); diff --git a/packages/gallery/src/interface/album.js b/packages/gallery/src/interface/album.js index 6d51958..250af35 100644 --- a/packages/gallery/src/interface/album.js +++ b/packages/gallery/src/interface/album.js @@ -1,31 +1,75 @@ import { defineView, defineElement as el } from 'domvm'; import * as image from '../data/image.js'; import { ImageView } from './image.js'; +import { LiveArray } from '../utils/livearray.js'; + +// Warn if overriding existing method +if (Array.prototype.equals) + console.warn( + "Overriding existing Array.prototype.equals. Possible causes: New API defines the method, there's a framework conflict or you've got double inclusions in your code." + ); +// attach the .equals method to Array's prototype to call it on any array +Array.prototype.equals = function(array) { + // if the other array is a falsy value, return + if (!array) return false; + + // compare lengths - can save a lot of time + if (this.length != array.length) return false; + + for (var i = 0, l = this.length; i < l; i++) { + // Check if we have nested arrays + if (this[i] instanceof Array && array[i] instanceof Array) { + // recurse into the nested arrays + if (!this[i].equals(array[i])) return false; + } else if (this[i] != array[i]) { + // Warning - two different object instances will never be equal: {x:20} != {x:20} + return false; + } + } + return true; +}; +// Hide method from for-in loops +Object.defineProperty(Array.prototype, 'equals', { enumerable: false }); export function AlbumView(vm, model) { - const { albumRow, remove } = model; - const { props, members } = albumRow.doc; - const title = props.title; - let images = []; - - // FIXME - If the album is updated, this does not properly refresh. - image.find(members, { attachments: true }).then(res => { - images = res.rows.filter(i => i.doc); - vm.redraw(); - }); + const { remove } = model; + let data = null; + let currentMembers = []; + let title = null; function removeImageFromAlbum(id, rev) { remove(title, id); } return function(vm, model, key, opts) { + const { doc, remove } = model; + const { props, members } = doc; + + if (title !== props.title || currentMembers.length !== members.length) { + if (data) { + data.cleanup(); + } + title = props.title; + currentMembers = members; + const SELECTOR = { + $or: [ + Object.assign({ [`tags.${title}`]: { $eq: true } }, image.SELECTOR), + { _id: { $in: members } } + ] + }; + + data = LiveArray(db, SELECTOR); + data.subscribe(() => vm.redraw()); + } + const images = data(); + return el('.album', [ el('h2', [title]), ...images.map(i => { return defineView( ImageView, { - imageRow: i, + doc: i, showTags: false, remove: removeImageFromAlbum }, diff --git a/packages/gallery/src/interface/gallery.js b/packages/gallery/src/interface/gallery.js index 68f5cc0..14348bb 100644 --- a/packages/gallery/src/interface/gallery.js +++ b/packages/gallery/src/interface/gallery.js @@ -1,17 +1,47 @@ import { defineView, defineElement as el } from 'domvm'; import * as image from '../data/image.js'; +import * as index from '../data/indexType.js'; import * as imageTag from '../context/manageImageTags.js'; import { ImageView } from './image.js'; import { AlbumView } from './album.js'; -import { router } from '../services/router.js'; +import { router, routeChanged } from '../services/router.js'; +import { LiveArray } from '../utils/livearray.js'; + +const NAV_OPTIONS = { + images: { + selector: image.SELECTOR, + title: 'Images' + }, + albums: { + selector: index.SELECTOR, + title: 'Albums' + } +}; + +function uploadImages(evt) { + image.add(evt.currentTarget.files); +} export function GalleryView(vm, model) { - function uploadImages(evt) { - image.add(evt.currentTarget.files); - } + let data = null; + let title = ''; + + routeChanged.subscribe(function onRouteChange(router, route) { + if (data) { + data.cleanup(); + } + const o = NAV_OPTIONS[route.name]; + data = LiveArray(db, o.selector); + title = o.title; + data.subscribe(() => vm.redraw()); + }); return function(vm, model, key, opts) { - const { title, members } = model; + if (!data || !data.ready()) { + return el('h1', 'Loading...'); + } + + const members = data(); return el('.gallery', [ el('input#fInput', { @@ -28,7 +58,7 @@ export function GalleryView(vm, model) { return defineView( ImageView, { - imageRow: i, + doc: i, showTags: true, addTag: imageTag.add, remove: image.remove, @@ -41,7 +71,7 @@ export function GalleryView(vm, model) { return defineView( AlbumView, { - albumRow: a, + doc: a, addTag: imageTag.add, remove: imageTag.remove }, diff --git a/packages/gallery/src/interface/image.js b/packages/gallery/src/interface/image.js index ce244ce..8f8795c 100644 --- a/packages/gallery/src/interface/image.js +++ b/packages/gallery/src/interface/image.js @@ -1,27 +1,46 @@ import { defineView, defineElement as el } from 'domvm'; +import { observable, computed } from 'frptools'; + +import * as image from '../data/image.js'; export function ImageView(vm, model) { const { addTag } = model; + const imageData = observable(null); + let imageId = null; function onAddTag(image_id) { addTag(prompt('Tag Name'), image_id); } return function(vm, model, key, opts) { - const { imageRow, showTags, remove, addTag, removeTag } = model; - const { doc } = imageRow; + const { doc, showTags, remove, removeTag } = model; const { _id: id, _rev: rev, tags } = doc; - const { thumbnail } = doc._attachments; const _showTags = showTags !== undefined ? showTags : true; const filteredTags = _showTags ? Object.entries(doc.tags).filter(([_, visible]) => visible) : []; + if (imageId !== id) { + image + .getAttachment(id, 'thumbnail') + .then(thumbnail => { + if (imageData()) { + URL.revokeObjectURL(imageData()); + } + imageData(URL.createObjectURL(thumbnail)); + vm.redraw(); + }) + .catch(err => { + // Probably hasn't created the thumbnail yet. + console.log("Probably hasn't created the thumbnail yet.", err); + imageId = null; + }); + imageId = id; + } - if (thumbnail) { - return el('div', [ + if (imageData()) { + return el('div', { _key: id }, [ el(`figure#${doc._id}.image`, [ el('img', { - src: `data:${thumbnail.content_type};base64,${thumbnail.data}`, + src: imageData(), title: `${id} ${name}`, - 'data-id': id, onclick: [remove, id, rev] }), filteredTags.length diff --git a/packages/gallery/src/utils/livearray.js b/packages/gallery/src/utils/livearray.js new file mode 100644 index 0000000..d1750df --- /dev/null +++ b/packages/gallery/src/utils/livearray.js @@ -0,0 +1,35 @@ +import { observable, computed } from 'frptools'; +import { group, groupEnd, log } from '../services/console.js'; +import { Watcher } from './watcher.js'; + +export function LiveArray(db, selector) { + const watcher = Watcher(db, selector); + const data = observable({ docs: [] }); + const docs = computed(r => r.docs, [data]); + let changeSub = null; + + const accessor = docs; + accessor.ready = observable(false); + accessor.cleanup = () => { + docs.detach(); + if (changeSub) { + changeSub(); + } + accessor.ready.unsubscribeAll(); + data({ docs: [] }); + }; + + async function refresh() { + group('LiveArray Refreshing'); + log(selector); + data(await db.find({ selector })); + log(data()); + groupEnd('LiveArray Refreshing'); + } + + refresh().then(() => { + changeSub = watcher(refresh); + accessor.ready(true); + }); + return accessor; +} diff --git a/packages/gallery/src/utils/watcher.js b/packages/gallery/src/utils/watcher.js index 269a047..03f1a6f 100644 --- a/packages/gallery/src/utils/watcher.js +++ b/packages/gallery/src/utils/watcher.js @@ -25,7 +25,7 @@ export function Watcher(db, selector) { }); } return () => { - this.subscribers.delete(fn); + subscribers.delete(fn); if (subscribers.size === 0 && changes) { log('Unwatching:', db, selector); changes.cancel();