Interface is fully DB-driven

While there are still events for things like maintaining indexes, those may be unnecessary and may go away.
This commit is contained in:
Timothy Farrell 2017-10-30 04:22:21 -05:00
parent 937713de53
commit a9a252f8b0
9 changed files with 177 additions and 66 deletions

View File

@ -14,6 +14,7 @@
"dependencies": { "dependencies": {
"domvm": "~3.2.0", "domvm": "~3.2.0",
"exif-parser": "~0.1.9", "exif-parser": "~0.1.9",
"frptools": "1.1.0",
"pica": "~2.0.8", "pica": "~2.0.8",
"pouchdb-adapter-http": "~6.3.4", "pouchdb-adapter-http": "~6.3.4",
"pouchdb-adapter-idb": "~6.3.4", "pouchdb-adapter-idb": "~6.3.4",

View File

@ -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 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 generateThumbnails from './contextLoaders/generateThumbnails.js';
import { GalleryView } from './interface/gallery.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(); window.db = getDatabase();
const NAV_OPTIONS = { // Watch for new images, generate thumbnails if they need them.
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);
}
image.watcher(generateThumbnails); 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, { // Attach our root view to the DOM
title: '', createView(GalleryView, {}).mount(document.querySelector('#app'));
members: []
}).mount(document.querySelector('#app'));
// Start the router
router.start('home'); router.start('home');

View File

@ -8,7 +8,7 @@ import { Watcher } from '../utils/watcher.js';
const db = getDatabase(); const db = getDatabase();
const PROCESS_PREFIX = 'importing'; const PROCESS_PREFIX = 'importing';
const PREFIX = 'image'; const PREFIX = 'image';
const SELECTOR = { export const SELECTOR = {
_id: { _id: {
$gt: `${PREFIX}_`, $gt: `${PREFIX}_`,
$lt: `${PREFIX}_\ufff0` $lt: `${PREFIX}_\ufff0`
@ -45,6 +45,10 @@ export async function find(keys, options = {}) {
return await db.allDocs(opts); return await db.allDocs(opts);
} }
export async function getAttachment(id, attName) {
return await db.getAttachment(id, attName);
}
export async function add(imageFileList) { export async function add(imageFileList) {
const docs = Array.prototype.map.call(imageFileList, f => ({ const docs = Array.prototype.map.call(imageFileList, f => ({
_id: `${PROCESS_PREFIX}_${f.name}`, _id: `${PROCESS_PREFIX}_${f.name}`,

View File

@ -4,6 +4,12 @@ import { Event } from '../utils/event.js';
const db = getDatabase(); const db = getDatabase();
const PREFIX = 'index'; const PREFIX = 'index';
export const SELECTOR = {
_id: {
$gt: `${PREFIX}_`,
$lt: `${PREFIX}_\ufff0`
}
};
// Events // Events
export const added = new Event('Index.added'); export const added = new Event('Index.added');

View File

@ -1,31 +1,75 @@
import { defineView, defineElement as el } from 'domvm'; import { defineView, defineElement as el } from 'domvm';
import * as image from '../data/image.js'; import * as image from '../data/image.js';
import { ImageView } from './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) { export function AlbumView(vm, model) {
const { albumRow, remove } = model; const { remove } = model;
const { props, members } = albumRow.doc; let data = null;
const title = props.title; let currentMembers = [];
let images = []; let title = null;
// 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();
});
function removeImageFromAlbum(id, rev) { function removeImageFromAlbum(id, rev) {
remove(title, id); remove(title, id);
} }
return function(vm, model, key, opts) { 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', [ return el('.album', [
el('h2', [title]), el('h2', [title]),
...images.map(i => { ...images.map(i => {
return defineView( return defineView(
ImageView, ImageView,
{ {
imageRow: i, doc: i,
showTags: false, showTags: false,
remove: removeImageFromAlbum remove: removeImageFromAlbum
}, },

View File

@ -1,17 +1,47 @@
import { defineView, defineElement as el } from 'domvm'; import { defineView, defineElement as el } from 'domvm';
import * as image from '../data/image.js'; import * as image from '../data/image.js';
import * as index from '../data/indexType.js';
import * as imageTag from '../context/manageImageTags.js'; import * as imageTag from '../context/manageImageTags.js';
import { ImageView } from './image.js'; import { ImageView } from './image.js';
import { AlbumView } from './album.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) { export function GalleryView(vm, model) {
function uploadImages(evt) { let data = null;
image.add(evt.currentTarget.files); 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) { return function(vm, model, key, opts) {
const { title, members } = model; if (!data || !data.ready()) {
return el('h1', 'Loading...');
}
const members = data();
return el('.gallery', [ return el('.gallery', [
el('input#fInput', { el('input#fInput', {
@ -28,7 +58,7 @@ export function GalleryView(vm, model) {
return defineView( return defineView(
ImageView, ImageView,
{ {
imageRow: i, doc: i,
showTags: true, showTags: true,
addTag: imageTag.add, addTag: imageTag.add,
remove: image.remove, remove: image.remove,
@ -41,7 +71,7 @@ export function GalleryView(vm, model) {
return defineView( return defineView(
AlbumView, AlbumView,
{ {
albumRow: a, doc: a,
addTag: imageTag.add, addTag: imageTag.add,
remove: imageTag.remove remove: imageTag.remove
}, },

View File

@ -1,27 +1,46 @@
import { defineView, defineElement as el } from 'domvm'; import { defineView, defineElement as el } from 'domvm';
import { observable, computed } from 'frptools';
import * as image from '../data/image.js';
export function ImageView(vm, model) { export function ImageView(vm, model) {
const { addTag } = model; const { addTag } = model;
const imageData = observable(null);
let imageId = null;
function onAddTag(image_id) { function onAddTag(image_id) {
addTag(prompt('Tag Name'), image_id); addTag(prompt('Tag Name'), image_id);
} }
return function(vm, model, key, opts) { return function(vm, model, key, opts) {
const { imageRow, showTags, remove, addTag, removeTag } = model; const { doc, showTags, remove, removeTag } = model;
const { doc } = imageRow;
const { _id: id, _rev: rev, tags } = doc; const { _id: id, _rev: rev, tags } = doc;
const { thumbnail } = doc._attachments;
const _showTags = showTags !== undefined ? showTags : true; const _showTags = showTags !== undefined ? showTags : true;
const filteredTags = _showTags ? Object.entries(doc.tags).filter(([_, visible]) => visible) : []; 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) { if (imageData()) {
return el('div', [ return el('div', { _key: id }, [
el(`figure#${doc._id}.image`, [ el(`figure#${doc._id}.image`, [
el('img', { el('img', {
src: `data:${thumbnail.content_type};base64,${thumbnail.data}`, src: imageData(),
title: `${id} ${name}`, title: `${id} ${name}`,
'data-id': id,
onclick: [remove, id, rev] onclick: [remove, id, rev]
}), }),
filteredTags.length filteredTags.length

View File

@ -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;
}

View File

@ -25,7 +25,7 @@ export function Watcher(db, selector) {
}); });
} }
return () => { return () => {
this.subscribers.delete(fn); subscribers.delete(fn);
if (subscribers.size === 0 && changes) { if (subscribers.size === 0 && changes) {
log('Unwatching:', db, selector); log('Unwatching:', db, selector);
changes.cancel(); changes.cancel();